@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.
Files changed (127) hide show
  1. package/README.md +1598 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/account/add.d.ts +16 -0
  7. package/dist/commands/account/add.js +185 -0
  8. package/dist/commands/account/list.d.ts +6 -0
  9. package/dist/commands/account/list.js +37 -0
  10. package/dist/commands/account/remove.d.ts +11 -0
  11. package/dist/commands/account/remove.js +57 -0
  12. package/dist/commands/auth/login.d.ts +16 -0
  13. package/dist/commands/auth/login.js +178 -0
  14. package/dist/commands/auth/logout.d.ts +6 -0
  15. package/dist/commands/auth/logout.js +39 -0
  16. package/dist/commands/auth/refresh.d.ts +6 -0
  17. package/dist/commands/auth/refresh.js +66 -0
  18. package/dist/commands/auth/status.d.ts +6 -0
  19. package/dist/commands/auth/status.js +63 -0
  20. package/dist/commands/ci/account/create.d.ts +16 -0
  21. package/dist/commands/ci/account/create.js +158 -0
  22. package/dist/commands/ci/account/delete.d.ts +14 -0
  23. package/dist/commands/ci/account/delete.js +88 -0
  24. package/dist/commands/ci/account/list.d.ts +10 -0
  25. package/dist/commands/ci/account/list.js +65 -0
  26. package/dist/commands/config/get.d.ts +9 -0
  27. package/dist/commands/config/get.js +37 -0
  28. package/dist/commands/config/set.d.ts +10 -0
  29. package/dist/commands/config/set.js +48 -0
  30. package/dist/commands/config/show.d.ts +6 -0
  31. package/dist/commands/config/show.js +10 -0
  32. package/dist/commands/deployment/create.d.ts +30 -0
  33. package/dist/commands/deployment/create.js +188 -0
  34. package/dist/commands/deployment/get.d.ts +13 -0
  35. package/dist/commands/deployment/get.js +101 -0
  36. package/dist/commands/deployment/launch.d.ts +15 -0
  37. package/dist/commands/deployment/launch.js +105 -0
  38. package/dist/commands/deployment/list.d.ts +11 -0
  39. package/dist/commands/deployment/list.js +91 -0
  40. package/dist/commands/domain/current.d.ts +6 -0
  41. package/dist/commands/domain/current.js +18 -0
  42. package/dist/commands/domain/list.d.ts +6 -0
  43. package/dist/commands/domain/list.js +42 -0
  44. package/dist/commands/domain/switch.d.ts +9 -0
  45. package/dist/commands/domain/switch.js +40 -0
  46. package/dist/commands/example.d.ts +13 -0
  47. package/dist/commands/example.js +24 -0
  48. package/dist/commands/git/connect.d.ts +10 -0
  49. package/dist/commands/git/connect.js +56 -0
  50. package/dist/commands/git/disconnect.d.ts +11 -0
  51. package/dist/commands/git/disconnect.js +93 -0
  52. package/dist/commands/git/list.d.ts +10 -0
  53. package/dist/commands/git/list.js +53 -0
  54. package/dist/commands/git/sync.d.ts +18 -0
  55. package/dist/commands/git/sync.js +235 -0
  56. package/dist/commands/init.d.ts +188 -0
  57. package/dist/commands/init.js +817 -0
  58. package/dist/commands/jira/connect.d.ts +9 -0
  59. package/dist/commands/jira/connect.js +141 -0
  60. package/dist/commands/jira/status.d.ts +9 -0
  61. package/dist/commands/jira/status.js +118 -0
  62. package/dist/commands/module/analyze.d.ts +29 -0
  63. package/dist/commands/module/analyze.js +201 -0
  64. package/dist/commands/module/create.d.ts +42 -0
  65. package/dist/commands/module/create.js +498 -0
  66. package/dist/commands/module/destroy.d.ts +11 -0
  67. package/dist/commands/module/destroy.js +77 -0
  68. package/dist/commands/module/get.d.ts +10 -0
  69. package/dist/commands/module/get.js +43 -0
  70. package/dist/commands/module/link.d.ts +15 -0
  71. package/dist/commands/module/link.js +175 -0
  72. package/dist/commands/module/list.d.ts +9 -0
  73. package/dist/commands/module/list.js +51 -0
  74. package/dist/commands/module/reanalyze.d.ts +30 -0
  75. package/dist/commands/module/reanalyze.js +206 -0
  76. package/dist/commands/module/update.d.ts +27 -0
  77. package/dist/commands/module/update.js +102 -0
  78. package/dist/commands/parameter/add.d.ts +15 -0
  79. package/dist/commands/parameter/add.js +99 -0
  80. package/dist/commands/parameter/backfill.d.ts +12 -0
  81. package/dist/commands/parameter/backfill.js +113 -0
  82. package/dist/commands/parameter/clear.d.ts +14 -0
  83. package/dist/commands/parameter/clear.js +95 -0
  84. package/dist/commands/parameter/list.d.ts +14 -0
  85. package/dist/commands/parameter/list.js +92 -0
  86. package/dist/commands/parameter/pull.d.ts +14 -0
  87. package/dist/commands/parameter/pull.js +124 -0
  88. package/dist/commands/parameter/remove.d.ts +15 -0
  89. package/dist/commands/parameter/remove.js +90 -0
  90. package/dist/commands/parameter/sync.d.ts +14 -0
  91. package/dist/commands/parameter/sync.js +153 -0
  92. package/dist/commands/parameter/update.d.ts +15 -0
  93. package/dist/commands/parameter/update.js +100 -0
  94. package/dist/commands/stage/create.d.ts +28 -0
  95. package/dist/commands/stage/create.js +312 -0
  96. package/dist/commands/stage/list.d.ts +9 -0
  97. package/dist/commands/stage/list.js +63 -0
  98. package/dist/commands/test-api.d.ts +9 -0
  99. package/dist/commands/test-api.js +40 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/services/auth-service.d.ts +84 -0
  103. package/dist/services/auth-service.js +240 -0
  104. package/dist/services/git.d.ts +46 -0
  105. package/dist/services/git.js +409 -0
  106. package/dist/services/hyperdrive-sigv4.d.ts +449 -0
  107. package/dist/services/hyperdrive-sigv4.js +375 -0
  108. package/dist/services/hyperdrive.d.ts +87 -0
  109. package/dist/services/hyperdrive.js +108 -0
  110. package/dist/services/log-tailer.d.ts +95 -0
  111. package/dist/services/log-tailer.js +242 -0
  112. package/dist/services/tenant-service.d.ts +106 -0
  113. package/dist/services/tenant-service.js +332 -0
  114. package/dist/utils/account-flow.d.ts +74 -0
  115. package/dist/utils/account-flow.js +228 -0
  116. package/dist/utils/auth-flow.d.ts +146 -0
  117. package/dist/utils/auth-flow.js +477 -0
  118. package/dist/utils/git-flow.d.ts +72 -0
  119. package/dist/utils/git-flow.js +232 -0
  120. package/dist/utils/jira-flow.d.ts +71 -0
  121. package/dist/utils/jira-flow.js +120 -0
  122. package/dist/utils/summary-display.d.ts +59 -0
  123. package/dist/utils/summary-display.js +140 -0
  124. package/dist/utils/validation.d.ts +15 -0
  125. package/dist/utils/validation.js +32 -0
  126. package/oclif.manifest.json +2819 -0
  127. package/package.json +112 -0
@@ -0,0 +1,498 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { execSync } from 'node:child_process';
5
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { basename, join } from 'node:path';
7
+ import open from 'open';
8
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
9
+ export default class ModuleCreate extends Command {
10
+ static description = 'Create a new project';
11
+ static examples = [
12
+ '<%= config.bin %> <%= command.id %> --name="New Project" --slug="project-slug"',
13
+ '<%= config.bin %> <%= command.id %> --name="API Service" --framework="Serverless Framework"',
14
+ ];
15
+ static flags = {
16
+ buildCommand: Flags.string({
17
+ description: 'Build command',
18
+ }),
19
+ buildDirectory: Flags.string({
20
+ description: 'Build output directory (e.g., dist, build, .next) - where compiled artifacts are output',
21
+ }),
22
+ buildFolder: Flags.string({
23
+ description: 'Build folder',
24
+ }),
25
+ buildRuntime: Flags.string({
26
+ description: 'Build runtime for Dockerfile template selection',
27
+ options: ['nodejs', 'python', 'go', 'rust', 'java', 'dotnet', 'ruby'],
28
+ }),
29
+ buildRuntimeVersion: Flags.string({
30
+ description: 'Build runtime version (e.g., 20 for Node, 3.12 for Python)',
31
+ }),
32
+ ciService: Flags.string({
33
+ description: 'CI service used',
34
+ options: ['github-actions', 'gitlab-ci', 'circle-ci', 'jenkins'],
35
+ }),
36
+ defaultBranch: Flags.string({
37
+ description: 'Default git branch to branch from (e.g., main, master)',
38
+ }),
39
+ domain: Flags.string({
40
+ char: 'd',
41
+ description: 'Tenant domain (for multi-domain setups)',
42
+ }),
43
+ framework: Flags.string({
44
+ description: 'Framework used',
45
+ options: ['express', 'fastify', 'koa', 'nextjs', 'react', 'vue', 'angular', 'svelte', 'flask', 'django', 'fastapi', 'gin', 'actix', 'spring', 'aspnet', 'rails', 'other'],
46
+ }),
47
+ installCommand: Flags.string({
48
+ description: 'Install command',
49
+ }),
50
+ name: Flags.string({
51
+ description: 'Name of the project',
52
+ }),
53
+ runCommand: Flags.string({
54
+ description: 'Run command',
55
+ }),
56
+ runtime: Flags.string({
57
+ char: 'r',
58
+ description: 'Runtime environment',
59
+ options: ['nodejs', 'python', 'go', 'rust', 'java', 'dotnet', 'ruby'],
60
+ }),
61
+ runtimeVersion: Flags.string({
62
+ description: 'Runtime version (e.g., 20, 3.12, 1.21)',
63
+ }),
64
+ slug: Flags.string({
65
+ description: 'Slug of the project',
66
+ }),
67
+ sourceDirectory: Flags.string({
68
+ description: 'Source code directory (e.g., src, lib, . for root) - default: src for Lambda, . for static',
69
+ }),
70
+ sourceLocation: Flags.string({
71
+ description: 'Source location of the project',
72
+ }),
73
+ };
74
+ async run() {
75
+ this.log(chalk.green('🚀 Let\'s create a new Hyperdrive project!'));
76
+ const { flags } = await this.parse(ModuleCreate);
77
+ const responses = await this.promptUser(flags);
78
+ const service = new HyperdriveSigV4Service(flags.domain);
79
+ const combinedData = { ...flags, ...responses };
80
+ try {
81
+ // Check if source location is a git URL and verify access
82
+ const sourceLocation = combinedData.sourceLocation;
83
+ const gitInfo = this.parseGitUrl(sourceLocation);
84
+ let gitConfig = {};
85
+ if (gitInfo) {
86
+ this.log(chalk.blue(`\n🔍 Checking access to ${gitInfo.provider} repository...`));
87
+ const accessResult = await service.gitCheckRepoAccess({
88
+ provider: gitInfo.provider,
89
+ repoUrl: sourceLocation,
90
+ });
91
+ if (!accessResult.hasAccess) {
92
+ this.log(chalk.yellow(`\n⚠️ Hyperdrive doesn't have access to this repository.`));
93
+ if (accessResult.needsInstallation) {
94
+ const confirmConnect = await inquirer.prompt([{
95
+ default: true,
96
+ message: `Would you like to connect your ${gitInfo.provider === 'github' ? 'GitHub' : 'GitLab'} account?`,
97
+ name: 'connect',
98
+ type: 'confirm',
99
+ }]);
100
+ if (confirmConnect.connect) {
101
+ // Initiate the auth flow
102
+ const authResponse = await service.gitAuthInitiate(gitInfo.provider);
103
+ this.log(chalk.yellow('\nOpening browser for authorization...'));
104
+ this.log(chalk.gray(`If the browser doesn't open, visit: ${authResponse.installUrl}`));
105
+ // Open the browser
106
+ await open(authResponse.installUrl);
107
+ this.log(chalk.gray('\nWaiting for authorization...'));
108
+ this.log(chalk.gray('Press Enter after you\'ve completed the authorization in your browser.'));
109
+ await inquirer.prompt([{
110
+ message: 'Press Enter to continue after authorizing...',
111
+ name: 'continue',
112
+ }]);
113
+ // Re-check access
114
+ const recheckResult = await service.gitCheckRepoAccess({
115
+ provider: gitInfo.provider,
116
+ repoUrl: sourceLocation,
117
+ });
118
+ if (!recheckResult.hasAccess) {
119
+ this.log(chalk.red('\n❌ Still no access to the repository.'));
120
+ this.log(chalk.gray('Make sure you granted access to the repository during authorization.'));
121
+ const continueAnyway = await inquirer.prompt([{
122
+ default: false,
123
+ message: 'Continue creating the module without git integration?',
124
+ name: 'continue',
125
+ type: 'confirm',
126
+ }]);
127
+ if (!continueAnyway.continue) {
128
+ this.error('Module creation cancelled.');
129
+ return;
130
+ }
131
+ }
132
+ else {
133
+ this.log(chalk.green('✅ Repository access confirmed!'));
134
+ // Note: gitProvider will be auto-detected from sourceLocation by the API
135
+ // Installation will be resolved dynamically at deployment time
136
+ gitConfig = {};
137
+ }
138
+ }
139
+ }
140
+ }
141
+ else {
142
+ this.log(chalk.green('✅ Repository access confirmed!'));
143
+ // Note: gitProvider will be auto-detected from sourceLocation by the API
144
+ // Installation will be resolved dynamically at deployment time
145
+ gitConfig = {};
146
+ }
147
+ }
148
+ // Filter out undefined values and ensure all required fields are strings
149
+ const cleanedData = Object.fromEntries(Object.entries({ ...combinedData, ...gitConfig }).filter(([_, value]) => value !== undefined));
150
+ // Convert sourceLocation from SSH to HTTPS format (API requirement)
151
+ if (cleanedData.sourceLocation) {
152
+ cleanedData.sourceLocation = this.convertToHttpsUrl(cleanedData.sourceLocation);
153
+ }
154
+ // Add gitProvider based on the sourceLocation
155
+ if (gitInfo) {
156
+ cleanedData.gitProvider = gitInfo.provider;
157
+ }
158
+ const result = await service.moduleCreate(cleanedData);
159
+ writeFileSync('.hyperdrive.json', JSON.stringify(result, null, 2), 'utf8');
160
+ this.log(chalk.blue('✨ Project created successfully! Here are the details:'));
161
+ this.log(JSON.stringify(result, null, 2));
162
+ // Post-create: Offer Dockerfile generation
163
+ if (gitInfo && result.slug) {
164
+ await this.offerDockerfileGeneration(service, result.slug);
165
+ }
166
+ }
167
+ catch (error) {
168
+ console.log('Error:', error);
169
+ this.error('An error occurred while creating the project:');
170
+ }
171
+ }
172
+ /**
173
+ * Convert any git URL (SSH or HTTPS) to HTTPS format for the API
174
+ */
175
+ convertToHttpsUrl(url) {
176
+ if (!url)
177
+ return url;
178
+ // Already HTTPS
179
+ if (url.startsWith('https://')) {
180
+ return url.replace(/\.git$/, '');
181
+ }
182
+ // Convert SSH to HTTPS
183
+ if (url.startsWith('git@github.com:')) {
184
+ return url
185
+ .replace('git@github.com:', 'https://github.com/')
186
+ .replace(/\.git$/, '');
187
+ }
188
+ if (url.startsWith('git@gitlab.com:')) {
189
+ return url
190
+ .replace('git@gitlab.com:', 'https://gitlab.com/')
191
+ .replace(/\.git$/, '');
192
+ }
193
+ // Return as-is if unknown format
194
+ return url;
195
+ }
196
+ getDefaultBranch() {
197
+ try {
198
+ // Try to get current branch
199
+ const currentBranch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
200
+ if (currentBranch) {
201
+ return currentBranch;
202
+ }
203
+ }
204
+ catch {
205
+ // Ignore errors
206
+ }
207
+ try {
208
+ // Try to get default branch from remote
209
+ const remoteBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
210
+ if (remoteBranch) {
211
+ return remoteBranch.replace('refs/remotes/origin/', '');
212
+ }
213
+ }
214
+ catch {
215
+ // Ignore errors
216
+ }
217
+ // Default fallback
218
+ return 'main';
219
+ }
220
+ getGitOrigin() {
221
+ try {
222
+ return execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ }
228
+ /**
229
+ * Offer AI-powered Dockerfile generation after project creation
230
+ */
231
+ async offerDockerfileGeneration(service, slug) {
232
+ this.log('');
233
+ this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
234
+ this.log(chalk.cyan('🤖 AI-Powered Dockerfile Generation'));
235
+ this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
236
+ this.log('');
237
+ this.log(chalk.gray('Hyperdrive can analyze your repository and generate an'));
238
+ this.log(chalk.gray('optimized Dockerfile that is validated to actually work.'));
239
+ this.log('');
240
+ const confirmAnalyze = await inquirer.prompt([{
241
+ default: true,
242
+ message: chalk.yellow('Would you like to generate a Dockerfile for this project?'),
243
+ name: 'analyze',
244
+ type: 'confirm',
245
+ }]);
246
+ if (!confirmAnalyze.analyze) {
247
+ this.log(chalk.gray('Skipping Dockerfile generation. You can run it later with:'));
248
+ this.log(chalk.white(` hyperdrive module analyze --slug ${slug}`));
249
+ return;
250
+ }
251
+ this.log('');
252
+ this.log(chalk.blue('🔍 Analyzing repository...'));
253
+ try {
254
+ const analyzeResult = await service.moduleAnalyze(slug);
255
+ this.log(chalk.green(`✅ Analysis queued! Job ID: ${analyzeResult.jobId}`));
256
+ this.log('');
257
+ this.log(chalk.gray('The AI is now:'));
258
+ this.log(chalk.gray(' 1. Cloning your repository'));
259
+ this.log(chalk.gray(' 2. Analyzing package.json, CI configs, and existing Dockerfiles'));
260
+ this.log(chalk.gray(' 3. Generating an optimized multi-stage Dockerfile'));
261
+ this.log(chalk.gray(' 4. Building and testing the image to verify it works'));
262
+ this.log('');
263
+ // Ask if they want to wait for the result
264
+ const confirmWait = await inquirer.prompt([{
265
+ default: false,
266
+ message: chalk.yellow('Would you like to wait for the Dockerfile to be generated? (This may take a few minutes)'),
267
+ name: 'wait',
268
+ type: 'confirm',
269
+ }]);
270
+ if (confirmWait.wait) {
271
+ await this.pollDockerfileStatus(service, slug);
272
+ }
273
+ else {
274
+ this.log('');
275
+ this.log(chalk.gray('You can check the status later with:'));
276
+ this.log(chalk.white(` hyperdrive module dockerfile --slug ${slug}`));
277
+ }
278
+ }
279
+ catch (error) {
280
+ this.log(chalk.yellow(`⚠️ Could not start Dockerfile analysis: ${error.message}`));
281
+ this.log(chalk.gray('You can try again later with:'));
282
+ this.log(chalk.white(` hyperdrive module analyze --slug ${slug}`));
283
+ }
284
+ }
285
+ parseGitUrl(url) {
286
+ if (!url)
287
+ return null;
288
+ // Handle SSH URLs (git@github.com:owner/repo.git)
289
+ if (url.startsWith('git@')) {
290
+ const githubMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
291
+ if (githubMatch) {
292
+ return { owner: githubMatch[1], provider: 'github', repo: githubMatch[2] };
293
+ }
294
+ const gitlabMatch = url.match(/git@gitlab\.com:([^/]+)\/(.+?)(?:\.git)?$/);
295
+ if (gitlabMatch) {
296
+ return { owner: gitlabMatch[1], provider: 'gitlab', repo: gitlabMatch[2] };
297
+ }
298
+ return null;
299
+ }
300
+ // Handle HTTPS URLs
301
+ try {
302
+ const parsed = new URL(url);
303
+ if (parsed.hostname === 'github.com' || parsed.hostname === 'www.github.com') {
304
+ const pathParts = parsed.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
305
+ if (pathParts.length >= 2) {
306
+ return { owner: pathParts[0], provider: 'github', repo: pathParts[1] };
307
+ }
308
+ }
309
+ if (parsed.hostname === 'gitlab.com' || parsed.hostname === 'www.gitlab.com') {
310
+ const pathParts = parsed.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
311
+ if (pathParts.length >= 2) {
312
+ return { owner: pathParts[0], provider: 'gitlab', repo: pathParts.slice(1).join('/') };
313
+ }
314
+ }
315
+ }
316
+ catch {
317
+ // Not a valid URL
318
+ }
319
+ return null;
320
+ }
321
+ /**
322
+ * Poll for Dockerfile generation status
323
+ */
324
+ async pollDockerfileStatus(service, slug) {
325
+ this.log('');
326
+ this.log(chalk.blue('⏳ Waiting for Dockerfile generation...'));
327
+ const maxAttempts = 60; // 5 minutes with 5s intervals
328
+ let attempts = 0;
329
+ while (attempts < maxAttempts) {
330
+ attempts++;
331
+ try {
332
+ const result = await service.moduleGetDockerfile(slug);
333
+ if (result) {
334
+ this.log('');
335
+ this.log(chalk.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
336
+ this.log(chalk.green('✅ Dockerfile Generated Successfully!'));
337
+ this.log(chalk.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
338
+ this.log('');
339
+ if (result.validated) {
340
+ this.log(chalk.green('🎉 The Dockerfile has been validated:'));
341
+ this.log(chalk.gray(` • Build: ${result.buildSuccess ? '✅ Passed' : '❌ Failed'}`));
342
+ this.log(chalk.gray(` • Run: ${result.runSuccess ? '✅ Passed' : '❌ Failed'}`));
343
+ this.log(chalk.gray(` • Port ${result.analysis?.port || 3000}: ${result.portOpen ? '✅ Open' : '❌ Not responding'}`));
344
+ }
345
+ else {
346
+ this.log(chalk.yellow('⚠️ The Dockerfile was generated but validation is pending.'));
347
+ }
348
+ this.log('');
349
+ this.log(chalk.blue('Analysis:'));
350
+ this.log(chalk.gray(` • Runtime: ${result.analysis?.detectedRuntime || 'unknown'}`));
351
+ this.log(chalk.gray(` • Framework: ${result.analysis?.detectedFramework || 'unknown'}`));
352
+ this.log(chalk.gray(` • Port: ${result.analysis?.port || 'unknown'}`));
353
+ if (result.analysis?.recommendations?.length) {
354
+ this.log('');
355
+ this.log(chalk.blue('Recommendations:'));
356
+ for (const rec of result.analysis.recommendations) {
357
+ this.log(chalk.gray(` • ${rec}`));
358
+ }
359
+ }
360
+ this.log('');
361
+ this.log(chalk.gray('To view the full Dockerfile, run:'));
362
+ this.log(chalk.white(` hyperdrive module dockerfile --slug ${slug}`));
363
+ return;
364
+ }
365
+ }
366
+ catch {
367
+ // Dockerfile not ready yet, continue polling
368
+ }
369
+ // Wait 5 seconds before next attempt
370
+ await new Promise(resolve => setTimeout(resolve, 5000));
371
+ process.stdout.write('.');
372
+ }
373
+ this.log('');
374
+ this.log(chalk.yellow('⏱️ Dockerfile generation is taking longer than expected.'));
375
+ this.log(chalk.gray('You can check the status later with:'));
376
+ this.log(chalk.white(` hyperdrive module dockerfile --slug ${slug}`));
377
+ }
378
+ async promptUser(flags) {
379
+ const prompts = [];
380
+ if (!flags.name) {
381
+ prompts.push({
382
+ message: chalk.yellow('📛 Enter the project name:'),
383
+ name: 'name',
384
+ validate: (input) => Boolean(input) || 'Project name is required.',
385
+ });
386
+ }
387
+ if (!flags.slug) {
388
+ prompts.push({
389
+ default: () => this.suggestSlug(),
390
+ message: chalk.yellow('🔗 Enter the project slug:'),
391
+ name: 'slug',
392
+ });
393
+ }
394
+ if (!flags.sourceLocation) {
395
+ prompts.push({
396
+ default: () => this.getGitOrigin(),
397
+ message: chalk.yellow('🗂️ Where is the source code located?'),
398
+ name: 'sourceLocation',
399
+ });
400
+ }
401
+ if (!flags.defaultBranch) {
402
+ prompts.push({
403
+ default: () => this.getDefaultBranch(),
404
+ message: chalk.yellow('🌿 What is the default branch to create stage branches from?'),
405
+ name: 'defaultBranch',
406
+ validate: (input) => Boolean(input) || 'Default branch is required.',
407
+ });
408
+ }
409
+ if (!flags.runtime) {
410
+ prompts.push({
411
+ choices: [
412
+ { name: 'Node.js', value: 'nodejs' },
413
+ { name: 'Python', value: 'python' },
414
+ { name: 'Go', value: 'go' },
415
+ { name: 'Rust', value: 'rust' },
416
+ { name: 'Java', value: 'java' },
417
+ { name: '.NET', value: 'dotnet' },
418
+ { name: 'Ruby', value: 'ruby' },
419
+ ],
420
+ default: 'nodejs',
421
+ message: chalk.yellow('⚡ What is the runtime?'),
422
+ name: 'runtime',
423
+ type: 'list',
424
+ });
425
+ }
426
+ if (!flags.runtimeVersion) {
427
+ prompts.push({
428
+ default: '20',
429
+ message: chalk.yellow('📦 What runtime version? (e.g., 20 for Node, 3.12 for Python)'),
430
+ name: 'runtimeVersion',
431
+ });
432
+ }
433
+ if (!flags.buildRuntime) {
434
+ prompts.push({
435
+ choices: [
436
+ { name: 'Node.js', value: 'nodejs' },
437
+ { name: 'Python', value: 'python' },
438
+ { name: 'Go', value: 'go' },
439
+ { name: 'Rust', value: 'rust' },
440
+ { name: 'Java', value: 'java' },
441
+ { name: '.NET', value: 'dotnet' },
442
+ { name: 'Ruby', value: 'ruby' },
443
+ ],
444
+ default: 'nodejs',
445
+ message: chalk.yellow('🐳 What build runtime should be used for the Docker template?'),
446
+ name: 'buildRuntime',
447
+ type: 'list',
448
+ });
449
+ }
450
+ if (!flags.framework) {
451
+ prompts.push({
452
+ choices: [
453
+ { name: 'Express.js', value: 'express' },
454
+ { name: 'Fastify', value: 'fastify' },
455
+ { name: 'Koa', value: 'koa' },
456
+ { name: 'Next.js', value: 'nextjs' },
457
+ { name: 'React', value: 'react' },
458
+ { name: 'Vue', value: 'vue' },
459
+ { name: 'Angular', value: 'angular' },
460
+ { name: 'Svelte', value: 'svelte' },
461
+ { name: 'Flask', value: 'flask' },
462
+ { name: 'Django', value: 'django' },
463
+ { name: 'FastAPI', value: 'fastapi' },
464
+ { name: 'Gin (Go)', value: 'gin' },
465
+ { name: 'Actix (Rust)', value: 'actix' },
466
+ { name: 'Spring (Java)', value: 'spring' },
467
+ { name: 'ASP.NET', value: 'aspnet' },
468
+ { name: 'Rails', value: 'rails' },
469
+ { name: 'Other', value: 'other' },
470
+ ],
471
+ message: chalk.yellow('🏗️ What is the framework?'),
472
+ name: 'framework',
473
+ type: 'list',
474
+ });
475
+ }
476
+ if (!flags.buildCommand) {
477
+ prompts.push({
478
+ message: chalk.yellow('🔨 What is the build command?'),
479
+ name: 'buildCommand',
480
+ });
481
+ }
482
+ if (!flags.runCommand) {
483
+ prompts.push({
484
+ message: chalk.yellow('🚀 What is the command to run the application in production/live environments?'),
485
+ name: 'runCommand',
486
+ });
487
+ }
488
+ return inquirer.prompt(prompts);
489
+ }
490
+ suggestSlug() {
491
+ const packagePath = join(process.cwd(), 'package.json');
492
+ if (existsSync(packagePath)) {
493
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
494
+ return packageJson.name.replaceAll(/[^\dA-Za-z-]/g, '-').toLowerCase();
495
+ }
496
+ return basename(process.cwd()).replaceAll(/[^\dA-Za-z-]/g, '-').toLowerCase();
497
+ }
498
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ModuleDestroy 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
+ force: 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
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,77 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
6
+ export default class ModuleDestroy extends Command {
7
+ static description = 'Destroy/delete a module permanently';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %> --slug="my-module"',
10
+ '<%= config.bin %> <%= command.id %> --slug="old-project" --force',
11
+ ];
12
+ static flags = {
13
+ domain: Flags.string({
14
+ char: 'd',
15
+ description: 'Tenant domain (for multi-domain setups)',
16
+ }),
17
+ force: Flags.boolean({
18
+ char: 'f',
19
+ default: false,
20
+ description: 'Skip confirmation prompt (DANGEROUS)',
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 destroy',
32
+ required: true,
33
+ }),
34
+ };
35
+ async run() {
36
+ const { flags } = await this.parse(ModuleDestroy);
37
+ if (!flags.force) {
38
+ this.log(chalk.red('⚠️ WARNING: This will permanently DELETE the module and all its data!'));
39
+ this.log(chalk.red('⚠️ This action CANNOT be undone!'));
40
+ const typeConfirmation = await inquirer.prompt([
41
+ {
42
+ message: chalk.red('Type the module slug to confirm deletion:'),
43
+ name: 'confirmSlug',
44
+ validate: (input) => input === flags.slug || `You must type "${flags.slug}" to confirm`,
45
+ },
46
+ ]);
47
+ if (typeConfirmation.confirmSlug !== flags.slug) {
48
+ this.log(chalk.yellow('Operation cancelled.'));
49
+ return;
50
+ }
51
+ const confirmation = await inquirer.prompt([
52
+ {
53
+ default: false,
54
+ message: chalk.red(`Are you ABSOLUTELY SURE you want to destroy module ${chalk.cyan(flags.slug)}?`),
55
+ name: 'confirm',
56
+ type: 'confirm',
57
+ },
58
+ ]);
59
+ if (!confirmation.confirm) {
60
+ this.log(chalk.yellow('Operation cancelled.'));
61
+ return;
62
+ }
63
+ }
64
+ try {
65
+ const service = new HyperdriveSigV4Service(flags.domain);
66
+ this.log(chalk.blue('🗑️ Destroying module...'));
67
+ await service.moduleDestroy({
68
+ slug: flags.slug,
69
+ });
70
+ this.log(chalk.green(`✅ Module ${flags.slug} destroyed successfully!`));
71
+ }
72
+ catch (error) {
73
+ console.error('Error:', error);
74
+ this.error('An error occurred while destroying the module');
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ModuleGet 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
+ slug: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,43 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ export default class ModuleGet extends Command {
6
+ static description = 'Get details of a specific module/project';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> --slug="my-module"',
9
+ '<%= config.bin %> <%= command.id %>',
10
+ ];
11
+ static flags = {
12
+ domain: Flags.string({
13
+ char: 'd',
14
+ description: 'Tenant domain (for multi-domain setups)',
15
+ }),
16
+ slug: Flags.string({
17
+ char: 's',
18
+ default() {
19
+ if (existsSync('.hyperdrive.json')) {
20
+ const hyperdriveConfig = JSON.parse(readFileSync('.hyperdrive.json', 'utf8'));
21
+ return hyperdriveConfig.slug;
22
+ }
23
+ return '';
24
+ },
25
+ description: 'Module slug to get details for',
26
+ required: true,
27
+ }),
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(ModuleGet);
31
+ try {
32
+ const service = new HyperdriveSigV4Service(flags.domain);
33
+ this.log(chalk.blue(`🔍 Fetching module ${chalk.cyan(flags.slug)}...`));
34
+ const module = await service.moduleGet({ slug: flags.slug });
35
+ this.log(chalk.green('✅ Module details:\n'));
36
+ this.log(JSON.stringify(module, null, 2));
37
+ }
38
+ catch (error) {
39
+ console.error('Error:', error);
40
+ this.error(`An error occurred while fetching module ${flags.slug}`);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ModuleLink 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
+ originSlug: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ parameter: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[], import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ targetSlug: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ };
11
+ confirmDeploymentBehavior(): Promise<any>;
12
+ prompt(): Promise<void>;
13
+ promptForEnvironmentVariables(): Promise<string[]>;
14
+ run(): Promise<void>;
15
+ }