@hyperdrive.bot/cli 1.0.2
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 +1598 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +3 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/account/add.d.ts +16 -0
- package/dist/commands/account/add.js +185 -0
- package/dist/commands/account/list.d.ts +6 -0
- package/dist/commands/account/list.js +37 -0
- package/dist/commands/account/remove.d.ts +11 -0
- package/dist/commands/account/remove.js +57 -0
- package/dist/commands/auth/login.d.ts +16 -0
- package/dist/commands/auth/login.js +178 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.js +39 -0
- package/dist/commands/auth/refresh.d.ts +6 -0
- package/dist/commands/auth/refresh.js +66 -0
- package/dist/commands/auth/status.d.ts +6 -0
- package/dist/commands/auth/status.js +63 -0
- package/dist/commands/ci/account/create.d.ts +16 -0
- package/dist/commands/ci/account/create.js +158 -0
- package/dist/commands/ci/account/delete.d.ts +14 -0
- package/dist/commands/ci/account/delete.js +88 -0
- package/dist/commands/ci/account/list.d.ts +10 -0
- package/dist/commands/ci/account/list.js +65 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +37 -0
- package/dist/commands/config/set.d.ts +10 -0
- package/dist/commands/config/set.js +48 -0
- package/dist/commands/config/show.d.ts +6 -0
- package/dist/commands/config/show.js +10 -0
- package/dist/commands/deployment/create.d.ts +30 -0
- package/dist/commands/deployment/create.js +188 -0
- package/dist/commands/deployment/get.d.ts +13 -0
- package/dist/commands/deployment/get.js +101 -0
- package/dist/commands/deployment/launch.d.ts +15 -0
- package/dist/commands/deployment/launch.js +105 -0
- package/dist/commands/deployment/list.d.ts +11 -0
- package/dist/commands/deployment/list.js +91 -0
- package/dist/commands/domain/current.d.ts +6 -0
- package/dist/commands/domain/current.js +18 -0
- package/dist/commands/domain/list.d.ts +6 -0
- package/dist/commands/domain/list.js +42 -0
- package/dist/commands/domain/switch.d.ts +9 -0
- package/dist/commands/domain/switch.js +40 -0
- package/dist/commands/example.d.ts +13 -0
- package/dist/commands/example.js +24 -0
- package/dist/commands/git/connect.d.ts +10 -0
- package/dist/commands/git/connect.js +56 -0
- package/dist/commands/git/disconnect.d.ts +11 -0
- package/dist/commands/git/disconnect.js +93 -0
- package/dist/commands/git/list.d.ts +10 -0
- package/dist/commands/git/list.js +53 -0
- package/dist/commands/git/sync.d.ts +18 -0
- package/dist/commands/git/sync.js +235 -0
- package/dist/commands/init.d.ts +188 -0
- package/dist/commands/init.js +817 -0
- package/dist/commands/jira/connect.d.ts +9 -0
- package/dist/commands/jira/connect.js +141 -0
- package/dist/commands/jira/status.d.ts +9 -0
- package/dist/commands/jira/status.js +118 -0
- package/dist/commands/module/analyze.d.ts +29 -0
- package/dist/commands/module/analyze.js +201 -0
- package/dist/commands/module/create.d.ts +42 -0
- package/dist/commands/module/create.js +498 -0
- package/dist/commands/module/destroy.d.ts +11 -0
- package/dist/commands/module/destroy.js +77 -0
- package/dist/commands/module/get.d.ts +10 -0
- package/dist/commands/module/get.js +43 -0
- package/dist/commands/module/link.d.ts +15 -0
- package/dist/commands/module/link.js +175 -0
- package/dist/commands/module/list.d.ts +9 -0
- package/dist/commands/module/list.js +51 -0
- package/dist/commands/module/reanalyze.d.ts +30 -0
- package/dist/commands/module/reanalyze.js +206 -0
- package/dist/commands/module/update.d.ts +27 -0
- package/dist/commands/module/update.js +102 -0
- package/dist/commands/parameter/add.d.ts +15 -0
- package/dist/commands/parameter/add.js +99 -0
- package/dist/commands/parameter/backfill.d.ts +12 -0
- package/dist/commands/parameter/backfill.js +113 -0
- package/dist/commands/parameter/clear.d.ts +14 -0
- package/dist/commands/parameter/clear.js +95 -0
- package/dist/commands/parameter/list.d.ts +14 -0
- package/dist/commands/parameter/list.js +92 -0
- package/dist/commands/parameter/pull.d.ts +14 -0
- package/dist/commands/parameter/pull.js +124 -0
- package/dist/commands/parameter/remove.d.ts +15 -0
- package/dist/commands/parameter/remove.js +90 -0
- package/dist/commands/parameter/sync.d.ts +14 -0
- package/dist/commands/parameter/sync.js +153 -0
- package/dist/commands/parameter/update.d.ts +15 -0
- package/dist/commands/parameter/update.js +100 -0
- package/dist/commands/stage/create.d.ts +28 -0
- package/dist/commands/stage/create.js +312 -0
- package/dist/commands/stage/list.d.ts +9 -0
- package/dist/commands/stage/list.js +63 -0
- package/dist/commands/test-api.d.ts +9 -0
- package/dist/commands/test-api.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/auth-service.d.ts +84 -0
- package/dist/services/auth-service.js +240 -0
- package/dist/services/git.d.ts +46 -0
- package/dist/services/git.js +409 -0
- package/dist/services/hyperdrive-sigv4.d.ts +449 -0
- package/dist/services/hyperdrive-sigv4.js +375 -0
- package/dist/services/hyperdrive.d.ts +87 -0
- package/dist/services/hyperdrive.js +108 -0
- package/dist/services/log-tailer.d.ts +95 -0
- package/dist/services/log-tailer.js +242 -0
- package/dist/services/tenant-service.d.ts +106 -0
- package/dist/services/tenant-service.js +332 -0
- package/dist/utils/account-flow.d.ts +74 -0
- package/dist/utils/account-flow.js +228 -0
- package/dist/utils/auth-flow.d.ts +146 -0
- package/dist/utils/auth-flow.js +477 -0
- package/dist/utils/git-flow.d.ts +72 -0
- package/dist/utils/git-flow.js +232 -0
- package/dist/utils/jira-flow.d.ts +71 -0
- package/dist/utils/jira-flow.js +120 -0
- package/dist/utils/summary-display.d.ts +59 -0
- package/dist/utils/summary-display.js +140 -0
- package/dist/utils/validation.d.ts +15 -0
- package/dist/utils/validation.js +32 -0
- package/oclif.manifest.json +2819 -0
- package/package.json +112 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class JiraConnect extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
export default class JiraConnect extends Command {
|
|
7
|
+
static description = 'Register your Jira instance with Hyperdrive (run BEFORE installing the Forge app)';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --domain dev-squad.atlassian.net',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
domain: Flags.string({
|
|
14
|
+
char: 'd',
|
|
15
|
+
description: 'Your Jira domain (e.g., dev-squad.atlassian.net)',
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(JiraConnect);
|
|
20
|
+
this.log('');
|
|
21
|
+
this.log(chalk.blue.bold('š Hyperdrive Jira Integration Setup'));
|
|
22
|
+
this.log('');
|
|
23
|
+
this.log(chalk.dim('This wizard will pre-register your Jira instance with Hyperdrive.'));
|
|
24
|
+
this.log(chalk.dim('After registration, you\'ll install the Forge app from the Atlassian Marketplace.'));
|
|
25
|
+
this.log('');
|
|
26
|
+
// Create API service (automatically checks auth)
|
|
27
|
+
let apiService;
|
|
28
|
+
const spinner = ora('Checking authentication...').start();
|
|
29
|
+
try {
|
|
30
|
+
apiService = new HyperdriveSigV4Service();
|
|
31
|
+
spinner.succeed('Authenticated');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
spinner.fail('Not authenticated');
|
|
35
|
+
this.error(`${error.message}\n\n` +
|
|
36
|
+
`Please authenticate first with: ${chalk.cyan('hd auth login')}`);
|
|
37
|
+
}
|
|
38
|
+
// Get Jira domain
|
|
39
|
+
let jiraDomain = flags.domain;
|
|
40
|
+
if (!jiraDomain) {
|
|
41
|
+
this.log('');
|
|
42
|
+
const answers = await inquirer.prompt([
|
|
43
|
+
{
|
|
44
|
+
default: 'your-company.atlassian.net',
|
|
45
|
+
filter: (input) => {
|
|
46
|
+
// Normalize domain
|
|
47
|
+
return input
|
|
48
|
+
.replace(/^https?:\/\//, '')
|
|
49
|
+
.replace(/\/$/, '')
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
},
|
|
52
|
+
message: 'Enter your Jira domain:',
|
|
53
|
+
name: 'jiraDomain',
|
|
54
|
+
type: 'input',
|
|
55
|
+
validate: (input) => {
|
|
56
|
+
// Normalize and validate
|
|
57
|
+
const normalized = input
|
|
58
|
+
.replace(/^https?:\/\//, '')
|
|
59
|
+
.replace(/\/$/, '')
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
if (!normalized.includes('.atlassian.net')) {
|
|
62
|
+
return 'Invalid Jira domain. Expected format: your-site.atlassian.net';
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]);
|
|
68
|
+
jiraDomain = answers.jiraDomain;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Normalize provided domain
|
|
72
|
+
jiraDomain = jiraDomain
|
|
73
|
+
.replace(/^https?:\/\//, '')
|
|
74
|
+
.replace(/\/$/, '')
|
|
75
|
+
.toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
// Pre-register domain
|
|
78
|
+
spinner.start('Registering Jira domain with Hyperdrive...');
|
|
79
|
+
try {
|
|
80
|
+
const response = await apiService['makeSignedRequest']('POST', '/hyperdrive/jira/pre-register', {
|
|
81
|
+
jiraDomain,
|
|
82
|
+
});
|
|
83
|
+
spinner.succeed('Jira domain registered successfully');
|
|
84
|
+
// Display registration details
|
|
85
|
+
this.log('');
|
|
86
|
+
this.log(chalk.green('ā
Pre-registration Complete!'));
|
|
87
|
+
this.log('');
|
|
88
|
+
this.log(chalk.bold('Registration Details:'));
|
|
89
|
+
this.log(` Jira Domain: ${chalk.cyan(response.registration.jiraDomain)}`);
|
|
90
|
+
this.log(` Tenant ID: ${chalk.dim(response.registration.tenantId)}`);
|
|
91
|
+
this.log(` Registration Token: ${chalk.cyan(response.registration.token)}`);
|
|
92
|
+
this.log(` Status: ${chalk.yellow(response.registration.status)}`);
|
|
93
|
+
this.log('');
|
|
94
|
+
this.log(chalk.bold('Next Steps:'));
|
|
95
|
+
response.nextSteps.instructions.forEach((instruction, index) => {
|
|
96
|
+
this.log(` ${chalk.cyan(`${index + 1}.`)} ${instruction}`);
|
|
97
|
+
});
|
|
98
|
+
this.log('');
|
|
99
|
+
this.log(chalk.bold('Marketplace URL:'));
|
|
100
|
+
this.log(` ${chalk.cyan(response.nextSteps.marketplaceUrl)}`);
|
|
101
|
+
this.log('');
|
|
102
|
+
this.log(chalk.dim('š” Tip: After installing the Forge app, run:'));
|
|
103
|
+
this.log(chalk.dim(` ${chalk.cyan('hd jira status')} - to verify the connection`));
|
|
104
|
+
this.log('');
|
|
105
|
+
this.log(chalk.yellow('ā ļø Important: You must install the Forge app for the integration to work!'));
|
|
106
|
+
this.log('');
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
spinner.fail('Failed to register Jira domain');
|
|
110
|
+
// Parse error message
|
|
111
|
+
let errorMessage = error.message;
|
|
112
|
+
if (error.response) {
|
|
113
|
+
const status = error.response.status;
|
|
114
|
+
const data = error.response.data;
|
|
115
|
+
if (status === 401) {
|
|
116
|
+
errorMessage = 'Authentication failed - please run "hd auth login"';
|
|
117
|
+
}
|
|
118
|
+
else if (status === 403) {
|
|
119
|
+
errorMessage = 'Access denied - check your permissions';
|
|
120
|
+
}
|
|
121
|
+
else if (status === 400 && data?.error) {
|
|
122
|
+
errorMessage = data.error;
|
|
123
|
+
}
|
|
124
|
+
else if (status === 404) {
|
|
125
|
+
errorMessage = 'Pre-register endpoint not found - is the API deployed?';
|
|
126
|
+
}
|
|
127
|
+
else if (data && data.error) {
|
|
128
|
+
errorMessage = data.error;
|
|
129
|
+
}
|
|
130
|
+
else if (data && data.message) {
|
|
131
|
+
errorMessage = data.message;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.error(`${errorMessage}\n\n` +
|
|
135
|
+
`Troubleshooting:\n` +
|
|
136
|
+
` ${chalk.cyan('ā¢')} Ensure you're authenticated: ${chalk.cyan('hd auth login')}\n` +
|
|
137
|
+
` ${chalk.cyan('ā¢')} Verify the API is deployed and accessible\n` +
|
|
138
|
+
` ${chalk.cyan('ā¢')} Check that the pre-register endpoint exists: /hyperdrive/jira/pre-register`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class JiraStatus extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
export default class JiraStatus extends Command {
|
|
6
|
+
static description = 'Check the status of your Jira integration';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %>',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
domain: Flags.string({
|
|
12
|
+
char: 'd',
|
|
13
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { flags } = await this.parse(JiraStatus);
|
|
18
|
+
this.log('');
|
|
19
|
+
this.log(chalk.blue.bold('š Hyperdrive Jira Integration Status'));
|
|
20
|
+
this.log('');
|
|
21
|
+
// Create API service (automatically checks auth)
|
|
22
|
+
let apiService;
|
|
23
|
+
const spinner = ora('Checking authentication...').start();
|
|
24
|
+
try {
|
|
25
|
+
apiService = new HyperdriveSigV4Service(flags.domain);
|
|
26
|
+
spinner.succeed('Authenticated');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
spinner.fail('Not authenticated');
|
|
30
|
+
this.error(`${error.message}\n\n` +
|
|
31
|
+
`Please authenticate first with: ${chalk.cyan('hd auth login')}`);
|
|
32
|
+
}
|
|
33
|
+
// Fetch status
|
|
34
|
+
spinner.start('Fetching Jira connection status...');
|
|
35
|
+
try {
|
|
36
|
+
const response = await apiService['makeSignedRequest']('GET', '/hyperdrive/jira/status');
|
|
37
|
+
spinner.succeed('Status retrieved');
|
|
38
|
+
// Display summary
|
|
39
|
+
this.log('');
|
|
40
|
+
this.log(chalk.bold('Summary:'));
|
|
41
|
+
this.log(` Tenant ID: ${chalk.dim(response.tenantId)}`);
|
|
42
|
+
this.log(` Active Connections: ${chalk.cyan(response.summary.activeConnections)}`);
|
|
43
|
+
this.log(` Total Connections: ${chalk.cyan(response.summary.totalConnections)}`);
|
|
44
|
+
this.log(` Pending Registrations: ${chalk.yellow(response.summary.pendingRegistrations)}`);
|
|
45
|
+
this.log('');
|
|
46
|
+
// Display active connections
|
|
47
|
+
if (response.connections.length > 0) {
|
|
48
|
+
this.log(chalk.bold('Active Jira Connections:'));
|
|
49
|
+
response.connections.forEach((conn, index) => {
|
|
50
|
+
const statusColor = conn.status === 'active' ? chalk.green : chalk.red;
|
|
51
|
+
this.log(` ${chalk.cyan(`${index + 1}.`)} ${chalk.cyan(conn.jiraDomain)}`);
|
|
52
|
+
this.log(` Cloud ID: ${chalk.dim(conn.cloudId)}`);
|
|
53
|
+
this.log(` Status: ${statusColor(conn.status)}`);
|
|
54
|
+
this.log(` Installed: ${chalk.dim(new Date(conn.installedAt).toLocaleString())}`);
|
|
55
|
+
});
|
|
56
|
+
this.log('');
|
|
57
|
+
}
|
|
58
|
+
// Display pending registrations
|
|
59
|
+
if (response.pendingRegistrations.length > 0) {
|
|
60
|
+
this.log(chalk.bold('Pending Registrations:'));
|
|
61
|
+
this.log(chalk.dim('These Jira instances are registered but the Forge app has not been installed yet.'));
|
|
62
|
+
this.log('');
|
|
63
|
+
response.pendingRegistrations.forEach((reg, index) => {
|
|
64
|
+
this.log(` ${chalk.yellow(`${index + 1}.`)} ${chalk.cyan(reg.jiraDomain)}`);
|
|
65
|
+
this.log(` Token: ${chalk.cyan(reg.token)}`);
|
|
66
|
+
this.log(` Status: ${chalk.yellow(reg.status)}`);
|
|
67
|
+
this.log(` Registered: ${chalk.dim(new Date(reg.registeredAt).toLocaleString())}`);
|
|
68
|
+
});
|
|
69
|
+
this.log('');
|
|
70
|
+
this.log(chalk.yellow('ā ļø Action Required: Install the Forge app from the Atlassian Marketplace'));
|
|
71
|
+
this.log(chalk.dim(` Run: ${chalk.cyan('hd jira connect')} to see installation instructions`));
|
|
72
|
+
this.log('');
|
|
73
|
+
}
|
|
74
|
+
// No connections or registrations
|
|
75
|
+
if (response.connections.length === 0 && response.pendingRegistrations.length === 0) {
|
|
76
|
+
this.log(chalk.yellow('No Jira connections found'));
|
|
77
|
+
this.log('');
|
|
78
|
+
this.log(chalk.dim('To connect a Jira instance, run:'));
|
|
79
|
+
this.log(chalk.dim(` ${chalk.cyan('hd jira connect')}`));
|
|
80
|
+
this.log('');
|
|
81
|
+
}
|
|
82
|
+
// Success message if everything is connected
|
|
83
|
+
if (response.summary.activeConnections > 0 && response.summary.pendingRegistrations === 0) {
|
|
84
|
+
this.log(chalk.green('ā
All Jira instances are connected and ready!'));
|
|
85
|
+
this.log('');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
spinner.fail('Failed to fetch status');
|
|
90
|
+
// Parse error message
|
|
91
|
+
let errorMessage = error.message;
|
|
92
|
+
if (error.response) {
|
|
93
|
+
const status = error.response.status;
|
|
94
|
+
const data = error.response.data;
|
|
95
|
+
if (status === 401) {
|
|
96
|
+
errorMessage = 'Authentication failed - please run "hd auth login"';
|
|
97
|
+
}
|
|
98
|
+
else if (status === 403) {
|
|
99
|
+
errorMessage = 'Access denied - check your permissions';
|
|
100
|
+
}
|
|
101
|
+
else if (status === 404) {
|
|
102
|
+
errorMessage = 'Status endpoint not found - is the API deployed?';
|
|
103
|
+
}
|
|
104
|
+
else if (data && data.error) {
|
|
105
|
+
errorMessage = data.error;
|
|
106
|
+
}
|
|
107
|
+
else if (data && data.message) {
|
|
108
|
+
errorMessage = data.message;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.error(`${errorMessage}\n\n` +
|
|
112
|
+
`Troubleshooting:\n` +
|
|
113
|
+
` ${chalk.cyan('ā¢')} Ensure you're authenticated: ${chalk.cyan('hd auth login')}\n` +
|
|
114
|
+
` ${chalk.cyan('ā¢')} Verify the API is deployed and accessible\n` +
|
|
115
|
+
` ${chalk.cyan('ā¢')} Check that the status endpoint exists: /hyperdrive/jira/status`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ModuleAnalyze extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
save: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
8
|
+
slug: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
verbose: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
wait: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Build CloudWatch console URL for log viewing
|
|
15
|
+
*/
|
|
16
|
+
private buildConsoleUrl;
|
|
17
|
+
/**
|
|
18
|
+
* Display analysis results
|
|
19
|
+
*/
|
|
20
|
+
private displayResults;
|
|
21
|
+
/**
|
|
22
|
+
* Poll for Dockerfile (fallback when streaming not available)
|
|
23
|
+
*/
|
|
24
|
+
private pollForDockerfile;
|
|
25
|
+
/**
|
|
26
|
+
* Stream analysis logs using CloudWatch
|
|
27
|
+
*/
|
|
28
|
+
private streamAnalysisLogs;
|
|
29
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
import { CloudWatchLogTailer } from '../../services/log-tailer.js';
|
|
6
|
+
export default class ModuleAnalyze extends Command {
|
|
7
|
+
static description = 'Analyze a module and generate an optimized Dockerfile using AI';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --slug="my-module"',
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --slug="my-module" --verbose',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
domain: Flags.string({
|
|
15
|
+
char: 'd',
|
|
16
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
17
|
+
}),
|
|
18
|
+
save: Flags.boolean({
|
|
19
|
+
default: false,
|
|
20
|
+
description: 'Save generated Dockerfile to current directory',
|
|
21
|
+
}),
|
|
22
|
+
slug: Flags.string({
|
|
23
|
+
char: 's',
|
|
24
|
+
default() {
|
|
25
|
+
if (existsSync('.hyperdrive.json')) {
|
|
26
|
+
const hyperdriveConfig = JSON.parse(readFileSync('.hyperdrive.json', 'utf8'));
|
|
27
|
+
return hyperdriveConfig.slug;
|
|
28
|
+
}
|
|
29
|
+
return '';
|
|
30
|
+
},
|
|
31
|
+
description: 'Module slug to analyze',
|
|
32
|
+
required: true,
|
|
33
|
+
}),
|
|
34
|
+
verbose: Flags.boolean({
|
|
35
|
+
char: 'v',
|
|
36
|
+
default: false,
|
|
37
|
+
description: 'Show detailed analysis progress and real-time logs',
|
|
38
|
+
}),
|
|
39
|
+
wait: Flags.boolean({
|
|
40
|
+
char: 'w',
|
|
41
|
+
default: true,
|
|
42
|
+
description: 'Wait for analysis to complete and show results',
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(ModuleAnalyze);
|
|
47
|
+
try {
|
|
48
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
49
|
+
this.log(chalk.blue(`š Analyzing module ${chalk.cyan(flags.slug)}...`));
|
|
50
|
+
this.log(chalk.gray('This will use AI to generate an optimized Dockerfile for your project.\n'));
|
|
51
|
+
if (flags.verbose) {
|
|
52
|
+
this.log(chalk.gray('š Verbose mode enabled - streaming real-time logs\n'));
|
|
53
|
+
}
|
|
54
|
+
const result = await service.moduleAnalyze(flags.slug);
|
|
55
|
+
this.log(chalk.green(`ā
Analysis queued successfully!`));
|
|
56
|
+
this.log(chalk.gray(` Job ID: ${result.jobId}`));
|
|
57
|
+
this.log(chalk.gray(` Status: ${result.status}`));
|
|
58
|
+
if (flags.verbose && result.logging) {
|
|
59
|
+
this.log(chalk.gray(` Project ID: ${result.projectId}`));
|
|
60
|
+
}
|
|
61
|
+
if (!flags.wait) {
|
|
62
|
+
this.log(chalk.yellow('\nUse --wait to poll for results, or check later with:'));
|
|
63
|
+
this.log(chalk.cyan(` hyperdrive module dockerfile --slug ${flags.slug}`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// If verbose and API returned logging config, stream logs
|
|
67
|
+
if (flags.verbose && result.logging) {
|
|
68
|
+
await this.streamAnalysisLogs(result.logging, flags, service);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Fallback to polling
|
|
72
|
+
if (flags.verbose && !result.logging) {
|
|
73
|
+
this.log(chalk.yellow('ā ļø Log streaming not available (API limitation), falling back to polling'));
|
|
74
|
+
}
|
|
75
|
+
this.log(chalk.blue('\nā³ Waiting for analysis to complete...'));
|
|
76
|
+
this.log(chalk.gray(' This typically takes 2-5 minutes.\n'));
|
|
77
|
+
const dockerfile = await this.pollForDockerfile(service, flags.slug);
|
|
78
|
+
if (!dockerfile) {
|
|
79
|
+
this.log(chalk.yellow('\nā ļø Analysis is still in progress.'));
|
|
80
|
+
this.log(chalk.gray(' Check back later with:'));
|
|
81
|
+
this.log(chalk.cyan(` hyperdrive module dockerfile --slug ${flags.slug}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await this.displayResults(dockerfile, flags);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('Error:', error);
|
|
89
|
+
this.error(`Failed to analyze module ${flags.slug}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build CloudWatch console URL for log viewing
|
|
94
|
+
*/
|
|
95
|
+
buildConsoleUrl(config) {
|
|
96
|
+
const encodedGroup = encodeURIComponent(encodeURIComponent(config.logGroup));
|
|
97
|
+
const encodedStream = encodeURIComponent(encodeURIComponent(config.logStream));
|
|
98
|
+
return `https://${config.region}.console.aws.amazon.com/cloudwatch/home?region=${config.region}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Display analysis results
|
|
102
|
+
*/
|
|
103
|
+
async displayResults(dockerfile, flags) {
|
|
104
|
+
if (!dockerfile) {
|
|
105
|
+
this.log(chalk.yellow('\nā ļø No Dockerfile found'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Show results
|
|
109
|
+
this.log(chalk.green('\nā
Dockerfile generated successfully!\n'));
|
|
110
|
+
if (dockerfile.analysis) {
|
|
111
|
+
this.log(chalk.blue('š Analysis Results:'));
|
|
112
|
+
this.log(chalk.gray(` Runtime: ${dockerfile.analysis.detectedRuntime || 'unknown'}`));
|
|
113
|
+
this.log(chalk.gray(` Framework: ${dockerfile.analysis.detectedFramework || 'unknown'}`));
|
|
114
|
+
if (dockerfile.analysis.port) {
|
|
115
|
+
this.log(chalk.gray(` Port: ${dockerfile.analysis.port}`));
|
|
116
|
+
}
|
|
117
|
+
if (dockerfile.analysis.healthEndpoint) {
|
|
118
|
+
this.log(chalk.gray(` Health: ${dockerfile.analysis.healthEndpoint}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.log(chalk.blue('\nš³ Validation Results:'));
|
|
122
|
+
this.log(chalk.gray(` Build: ${dockerfile.buildSuccess ? chalk.green('ā Success') : chalk.red('ā Failed')}`));
|
|
123
|
+
this.log(chalk.gray(` Run: ${dockerfile.runSuccess ? chalk.green('ā Success') : chalk.red('ā Failed')}`));
|
|
124
|
+
this.log(chalk.gray(` Port: ${dockerfile.portOpen ? chalk.green('ā Open') : chalk.red('ā Closed')}`));
|
|
125
|
+
if (dockerfile.analysis?.recommendations?.length) {
|
|
126
|
+
this.log(chalk.blue('\nš” Recommendations:'));
|
|
127
|
+
for (const rec of dockerfile.analysis.recommendations) {
|
|
128
|
+
this.log(chalk.gray(` ⢠${rec}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.log(chalk.blue('\nš Generated Dockerfile:'));
|
|
132
|
+
this.log(chalk.gray('ā'.repeat(60)));
|
|
133
|
+
this.log(dockerfile.dockerfile);
|
|
134
|
+
this.log(chalk.gray('ā'.repeat(60)));
|
|
135
|
+
if (flags.save) {
|
|
136
|
+
writeFileSync('Dockerfile', dockerfile.dockerfile);
|
|
137
|
+
this.log(chalk.green('\nā
Dockerfile saved to ./Dockerfile'));
|
|
138
|
+
if (dockerfile.dockerignore) {
|
|
139
|
+
writeFileSync('.dockerignore', dockerfile.dockerignore);
|
|
140
|
+
this.log(chalk.green('ā
.dockerignore saved to ./.dockerignore'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.log(chalk.gray('\nTip: Use --save to write files to current directory'));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Poll for Dockerfile (fallback when streaming not available)
|
|
149
|
+
*/
|
|
150
|
+
async pollForDockerfile(service, slug, maxAttempts = 60, intervalMs = 5000) {
|
|
151
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await service.moduleGetDockerfile(slug);
|
|
154
|
+
if (result) {
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Not ready yet
|
|
160
|
+
}
|
|
161
|
+
// Show progress
|
|
162
|
+
process.stdout.write(chalk.gray('.'));
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Stream analysis logs using CloudWatch
|
|
169
|
+
*/
|
|
170
|
+
async streamAnalysisLogs(loggingConfig, flags, service) {
|
|
171
|
+
const tailer = new CloudWatchLogTailer(loggingConfig, {
|
|
172
|
+
pollInterval: 1000,
|
|
173
|
+
showDebug: false,
|
|
174
|
+
verbose: true,
|
|
175
|
+
});
|
|
176
|
+
this.log('');
|
|
177
|
+
const outcome = await tailer.tail();
|
|
178
|
+
this.log('');
|
|
179
|
+
if (outcome.success) {
|
|
180
|
+
this.log(chalk.green(`ā
${outcome.message}`));
|
|
181
|
+
// Show CloudWatch console link
|
|
182
|
+
const consoleUrl = this.buildConsoleUrl(loggingConfig);
|
|
183
|
+
this.log(chalk.gray(` Logs: ${consoleUrl}`));
|
|
184
|
+
// Fetch generated Dockerfile
|
|
185
|
+
const dockerfile = await service.moduleGetDockerfile(flags.slug);
|
|
186
|
+
if (dockerfile) {
|
|
187
|
+
await this.displayResults(dockerfile, flags);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.log(chalk.yellow('\nā ļø Dockerfile not found yet, may still be processing'));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
this.log(chalk.red(`ā ${outcome.message}`));
|
|
195
|
+
// Show CloudWatch console link for debugging
|
|
196
|
+
const consoleUrl = this.buildConsoleUrl(loggingConfig);
|
|
197
|
+
this.log(chalk.gray(` Full logs: ${consoleUrl}`));
|
|
198
|
+
this.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ModuleCreate extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
buildCommand: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
buildDirectory: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
buildFolder: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
|
+
buildRuntime: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
buildRuntimeVersion: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
ciService: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
defaultBranch: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
|
+
framework: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
installCommand: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
16
|
+
name: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
17
|
+
runCommand: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
18
|
+
runtime: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
19
|
+
runtimeVersion: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
20
|
+
slug: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
21
|
+
sourceDirectory: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
22
|
+
sourceLocation: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
23
|
+
};
|
|
24
|
+
run(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Convert any git URL (SSH or HTTPS) to HTTPS format for the API
|
|
27
|
+
*/
|
|
28
|
+
private convertToHttpsUrl;
|
|
29
|
+
private getDefaultBranch;
|
|
30
|
+
private getGitOrigin;
|
|
31
|
+
/**
|
|
32
|
+
* Offer AI-powered Dockerfile generation after project creation
|
|
33
|
+
*/
|
|
34
|
+
private offerDockerfileGeneration;
|
|
35
|
+
private parseGitUrl;
|
|
36
|
+
/**
|
|
37
|
+
* Poll for Dockerfile generation status
|
|
38
|
+
*/
|
|
39
|
+
private pollDockerfileStatus;
|
|
40
|
+
private promptUser;
|
|
41
|
+
private suggestSlug;
|
|
42
|
+
}
|