@hyperdrive.bot/cli 1.0.12 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1495 -474
  2. package/dist/commands/deploy.d.ts +18 -0
  3. package/dist/commands/deploy.js +239 -0
  4. package/dist/commands/deployment/create.js +10 -2
  5. package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
  6. package/dist/commands/domain/set-production.js +27 -0
  7. package/dist/commands/git/list-open-prs.d.ts +12 -0
  8. package/dist/commands/git/list-open-prs.js +87 -0
  9. package/dist/commands/hook/add.d.ts +22 -0
  10. package/dist/commands/hook/add.js +299 -0
  11. package/dist/commands/hook/list.d.ts +11 -0
  12. package/dist/commands/hook/list.js +111 -0
  13. package/dist/commands/hook/logs.d.ts +13 -0
  14. package/dist/commands/hook/logs.js +124 -0
  15. package/dist/commands/hook/remove.d.ts +12 -0
  16. package/dist/commands/hook/remove.js +115 -0
  17. package/dist/commands/hook/toggle.d.ts +12 -0
  18. package/dist/commands/hook/toggle.js +125 -0
  19. package/dist/commands/init.d.ts +1 -1
  20. package/dist/commands/init.js +49 -9
  21. package/dist/commands/module/bindings.d.ts +14 -0
  22. package/dist/commands/module/bindings.js +125 -0
  23. package/dist/commands/module/create.d.ts +3 -0
  24. package/dist/commands/module/create.js +156 -78
  25. package/dist/commands/module/list.d.ts +1 -0
  26. package/dist/commands/module/list.js +22 -1
  27. package/dist/commands/module/sync.d.ts +29 -0
  28. package/dist/commands/module/sync.js +409 -0
  29. package/dist/commands/module/unlink.d.ts +11 -0
  30. package/dist/commands/module/unlink.js +77 -0
  31. package/dist/commands/module/update.d.ts +10 -0
  32. package/dist/commands/module/update.js +168 -5
  33. package/dist/commands/network/discover.d.ts +12 -0
  34. package/dist/commands/network/discover.js +210 -0
  35. package/dist/commands/network/get.d.ts +13 -0
  36. package/dist/commands/network/get.js +90 -0
  37. package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
  38. package/dist/commands/network/list.js +71 -0
  39. package/dist/commands/network/register.d.ts +16 -0
  40. package/dist/commands/network/register.js +144 -0
  41. package/dist/commands/parameter/sync.d.ts +13 -0
  42. package/dist/commands/parameter/sync.js +69 -1
  43. package/dist/commands/project/sync.d.ts +5 -11
  44. package/dist/commands/project/sync.js +12 -381
  45. package/dist/commands/seed.d.ts +93 -0
  46. package/dist/commands/seed.js +324 -0
  47. package/dist/commands/service/backup.d.ts +17 -0
  48. package/dist/commands/service/backup.js +156 -0
  49. package/dist/commands/service/backups.d.ts +14 -0
  50. package/dist/commands/service/backups.js +110 -0
  51. package/dist/commands/service/bind.d.ts +16 -0
  52. package/dist/commands/service/bind.js +106 -0
  53. package/dist/commands/service/bindings.d.ts +13 -0
  54. package/dist/commands/service/bindings.js +78 -0
  55. package/dist/commands/service/clone.d.ts +19 -0
  56. package/dist/commands/service/clone.js +153 -0
  57. package/dist/commands/service/create.d.ts +16 -0
  58. package/dist/commands/service/create.js +212 -0
  59. package/dist/commands/service/get.d.ts +13 -0
  60. package/dist/commands/service/get.js +97 -0
  61. package/dist/commands/service/list.d.ts +12 -0
  62. package/dist/commands/service/list.js +86 -0
  63. package/dist/commands/service/register.d.ts +21 -0
  64. package/dist/commands/service/register.js +215 -0
  65. package/dist/commands/service/restore.d.ts +19 -0
  66. package/dist/commands/service/restore.js +158 -0
  67. package/dist/commands/service/seed.d.ts +17 -0
  68. package/dist/commands/service/seed.js +173 -0
  69. package/dist/commands/service/templates.d.ts +10 -0
  70. package/dist/commands/service/templates.js +66 -0
  71. package/dist/commands/service/unbind.d.ts +15 -0
  72. package/dist/commands/service/unbind.js +74 -0
  73. package/dist/commands/stage/create.d.ts +23 -0
  74. package/dist/commands/stage/create.js +145 -6
  75. package/dist/commands/stage/delete.d.ts +11 -0
  76. package/dist/commands/stage/delete.js +85 -0
  77. package/dist/commands/stage/deploy.d.ts +34 -0
  78. package/dist/commands/stage/deploy.js +294 -0
  79. package/dist/commands/stage/ensure-branches.d.ts +23 -0
  80. package/dist/commands/stage/ensure-branches.js +101 -0
  81. package/dist/commands/stage/list.js +4 -0
  82. package/dist/commands/stage/status.d.ts +14 -0
  83. package/dist/commands/stage/status.js +100 -0
  84. package/dist/commands/{jira → tracker}/connect.js +32 -23
  85. package/dist/commands/tracker/hook/add.d.ts +25 -0
  86. package/dist/commands/tracker/hook/add.js +284 -0
  87. package/dist/commands/{jira → tracker}/hook/list.js +20 -11
  88. package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
  89. package/dist/commands/tracker/hook/logs.js +126 -0
  90. package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
  91. package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
  92. package/dist/commands/tracker/project/init.d.ts +17 -0
  93. package/dist/commands/tracker/project/init.js +178 -0
  94. package/dist/commands/tracker/project/link-module.d.ts +17 -0
  95. package/dist/commands/tracker/project/link-module.js +287 -0
  96. package/dist/commands/tracker/project/list-modules.d.ts +11 -0
  97. package/dist/commands/tracker/project/list-modules.js +117 -0
  98. package/dist/commands/tracker/project/list.d.ts +10 -0
  99. package/dist/commands/tracker/project/list.js +90 -0
  100. package/dist/commands/tracker/project/status.d.ts +13 -0
  101. package/dist/commands/tracker/project/status.js +168 -0
  102. package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
  103. package/dist/commands/tracker/project/unlink-module.js +251 -0
  104. package/dist/commands/{jira → tracker}/status.js +3 -3
  105. package/dist/lib/ensure-branches.d.ts +53 -0
  106. package/dist/lib/ensure-branches.js +149 -0
  107. package/dist/lib/git-providers/github.d.ts +16 -0
  108. package/dist/lib/git-providers/github.js +157 -0
  109. package/dist/lib/git-providers/gitlab.d.ts +16 -0
  110. package/dist/lib/git-providers/gitlab.js +148 -0
  111. package/dist/lib/git-providers/index.d.ts +67 -0
  112. package/dist/lib/git-providers/index.js +39 -0
  113. package/dist/lib/lambda-warmer.d.ts +106 -0
  114. package/dist/lib/lambda-warmer.js +189 -0
  115. package/dist/services/hyperdrive-sigv4.d.ts +360 -5
  116. package/dist/services/hyperdrive-sigv4.js +192 -24
  117. package/dist/utils/hook-flow.d.ts +60 -3
  118. package/dist/utils/hook-flow.js +437 -2
  119. package/dist/utils/hook-normalize.d.ts +6 -0
  120. package/dist/utils/hook-normalize.js +33 -0
  121. package/dist/utils/lifecycle-poller.d.ts +32 -0
  122. package/dist/utils/lifecycle-poller.js +72 -0
  123. package/dist/utils/retry.d.ts +43 -0
  124. package/dist/utils/retry.js +88 -0
  125. package/dist/utils/summary-display.js +1 -1
  126. package/dist/utils/tracker-project-flow.d.ts +84 -0
  127. package/dist/utils/tracker-project-flow.js +564 -0
  128. package/package.json +35 -7
  129. package/dist/commands/auth/login.d.ts +0 -16
  130. package/dist/commands/auth/login.js +0 -179
  131. package/dist/commands/auth/logout.js +0 -116
  132. package/dist/commands/auth/refresh.d.ts +0 -6
  133. package/dist/commands/auth/refresh.js +0 -66
  134. package/dist/commands/auth/status.d.ts +0 -6
  135. package/dist/commands/auth/status.js +0 -63
  136. package/dist/commands/config/get.d.ts +0 -9
  137. package/dist/commands/config/get.js +0 -37
  138. package/dist/commands/config/set.d.ts +0 -10
  139. package/dist/commands/config/set.js +0 -48
  140. package/dist/commands/config/show.d.ts +0 -6
  141. package/dist/commands/config/show.js +0 -10
  142. package/dist/commands/domain/current.d.ts +0 -6
  143. package/dist/commands/domain/current.js +0 -18
  144. package/dist/commands/domain/list.d.ts +0 -6
  145. package/dist/commands/domain/list.js +0 -42
  146. package/dist/commands/domain/switch.js +0 -40
  147. package/dist/commands/jira/hook/add.js +0 -147
  148. package/dist/services/tenant-service.d.ts +0 -127
  149. package/dist/services/tenant-service.js +0 -396
  150. package/dist/utils/auth-flow.d.ts +0 -147
  151. package/dist/utils/auth-flow.js +0 -479
  152. package/oclif.manifest.json +0 -3519
  153. /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
  154. /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
  155. /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
  156. /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
  157. /package/dist/commands/{jira → tracker}/status.d.ts +0 -0
@@ -7,6 +7,9 @@ export default class ModuleUpdate extends Command {
7
7
  '<%= config.bin %> <%= command.id %> --slug="my-module" --runtimeVersion="12"',
8
8
  '<%= config.bin %> <%= command.id %> --slug="my-module" --buildCommand="npm run build:prod"',
9
9
  '<%= config.bin %> <%= command.id %> --slug="my-module" --name="New Name" --framework="React.js"',
10
+ '<%= config.bin %> <%= command.id %> --slug="my-module" --postDeployCommand="npm run migrate" --postDeployFailureMode="warn"',
11
+ '<%= config.bin %> <%= command.id %> --slug="my-module" --runtime-config=\'{"timeout":60,"memory":4096}\'',
12
+ '<%= config.bin %> <%= command.id %> --slug="my-module" --show',
10
13
  ];
11
14
  static flags = {
12
15
  buildCommand: Flags.string({
@@ -32,9 +35,18 @@ export default class ModuleUpdate extends Command {
32
35
  defaultBranch: Flags.string({
33
36
  description: 'Default git branch to branch from (e.g., main, master)',
34
37
  }),
38
+ dependsOn: Flags.string({
39
+ description: 'JSON array of artifact dependencies: [{"artifactKey":"name","downloadTo":"path/"}]',
40
+ }),
35
41
  deploymentStrategy: Flags.string({
36
- description: 'Deployment strategy: "serverless" for Serverless Framework deployment to Lambda',
37
- options: ['serverless'],
42
+ description: 'Deployment strategy: "serverless" for SLS deploy, "docker-build" for Docker-based artifact builds',
43
+ options: ['serverless', 'docker-build'],
44
+ }),
45
+ dockerBuild: Flags.string({
46
+ description: 'Docker-build config JSON: {"image","buildCommand","artifactPaths","artifactKey","workdir","cache"}',
47
+ }),
48
+ 'deploy-command': Flags.string({
49
+ description: 'Custom LAUNCH phase deploy command (default: sls deploy --package .serverless --stage <stage> --region <region>). E.g., "npm run deploy:devsquad". BUILD phase uses buildCommand.',
38
50
  }),
39
51
  domain: Flags.string({
40
52
  char: 'd',
@@ -47,6 +59,17 @@ export default class ModuleUpdate extends Command {
47
59
  installCommand: Flags.string({
48
60
  description: 'Install command',
49
61
  }),
62
+ postDeployCommand: Flags.string({
63
+ description: 'Command to run after successful deploy',
64
+ }),
65
+ postDeployFailureMode: Flags.string({
66
+ description: 'Post-deploy command failure behavior: fail (default) or warn',
67
+ options: ['fail', 'warn'],
68
+ }),
69
+ 'module-type': Flags.string({
70
+ description: 'Module deployment type (determines API Gateway vs CloudFront vs CDK)',
71
+ options: ['backend', 'frontend', 'cdk'],
72
+ }),
50
73
  name: Flags.string({
51
74
  description: 'Name of the project',
52
75
  }),
@@ -54,18 +77,28 @@ export default class ModuleUpdate extends Command {
54
77
  allowNo: true,
55
78
  description: 'Enable AI-powered route discovery for per-route Lambda functions',
56
79
  }),
80
+ 'runtime-mode': Flags.string({
81
+ description: 'Runtime mode: "handler" for native Lambda handlers, "server" for HTTP server wrapping',
82
+ options: ['handler', 'server'],
83
+ }),
57
84
  runCommand: Flags.string({
58
85
  description: 'Run command',
59
86
  }),
60
87
  runtime: Flags.string({
61
88
  char: 'r',
62
89
  description: 'Runtime environment',
63
- options: ['nodejs', 'python', 'go', 'rust', 'java', 'dotnet', 'ruby'],
90
+ options: ['nodejs', 'python', 'go', 'rust', 'java', 'dotnet', 'ruby', 'static'],
91
+ }),
92
+ 'runtime-config': Flags.string({
93
+ description: 'Lambda runtime config JSON: {"timeout":60,"memory":4096}. timeout=1-900s, memory=128-10240MB.',
64
94
  }),
65
95
  runtimeVersion: Flags.string({
66
96
  char: 'v',
67
97
  description: 'Runtime version (e.g., 20, 3.12, 1.21)',
68
98
  }),
99
+ show: Flags.boolean({
100
+ description: 'Print current module config (including runtimeConfig) without updating',
101
+ }),
69
102
  slug: Flags.string({
70
103
  char: 's',
71
104
  description: 'Module slug to update (required)',
@@ -77,12 +110,142 @@ export default class ModuleUpdate extends Command {
77
110
  sourceLocation: Flags.string({
78
111
  description: 'Source location of the project',
79
112
  }),
113
+ subdomain: Flags.string({
114
+ description: 'Subdomain prefix for custom domain URLs (e.g., "api", "app", "www", "" for apex)',
115
+ }),
80
116
  };
81
117
  async run() {
82
118
  const { flags } = await this.parse(ModuleUpdate);
83
- const { slug, ...updateFields } = flags;
119
+ const { show, slug, ...updateFields } = flags;
120
+ // --show short-circuit: fetch and print the current module record without updating
121
+ if (show) {
122
+ const service = new HyperdriveSigV4Service(flags.domain);
123
+ try {
124
+ const module = await service.moduleGet({ slug });
125
+ const runtimeConfig = module.runtimeConfig;
126
+ this.log(chalk.green(`✅ Module "${slug}" runtimeConfig:`));
127
+ this.log(JSON.stringify(runtimeConfig ?? null, null, 2));
128
+ return;
129
+ }
130
+ catch (error) {
131
+ console.error('Error:', error);
132
+ this.error(`An error occurred while fetching module ${slug}`);
133
+ }
134
+ }
135
+ // Map kebab-case flags to camelCase API fields
136
+ const mappedFields = { ...updateFields };
137
+ if (mappedFields['module-type'] !== undefined) {
138
+ mappedFields.moduleType = mappedFields['module-type'];
139
+ delete mappedFields['module-type'];
140
+ }
141
+ if (mappedFields['deploy-command'] !== undefined) {
142
+ mappedFields.deployCommand = mappedFields['deploy-command'];
143
+ delete mappedFields['deploy-command'];
144
+ }
145
+ if (mappedFields['runtime-mode'] !== undefined) {
146
+ mappedFields.runtimeMode = mappedFields['runtime-mode'];
147
+ delete mappedFields['runtime-mode'];
148
+ }
149
+ if (mappedFields['post-deploy-command'] !== undefined) {
150
+ mappedFields.postDeployCommand = mappedFields['post-deploy-command'];
151
+ delete mappedFields['post-deploy-command'];
152
+ }
153
+ if (mappedFields['post-deploy-failure-mode'] !== undefined) {
154
+ mappedFields.postDeployFailureMode = mappedFields['post-deploy-failure-mode'];
155
+ delete mappedFields['post-deploy-failure-mode'];
156
+ }
157
+ // Parse and validate runtimeConfig JSON (Lambda timeout + memory)
158
+ if (mappedFields['runtime-config'] !== undefined) {
159
+ let parsed;
160
+ try {
161
+ parsed = JSON.parse(mappedFields['runtime-config']);
162
+ }
163
+ catch (error) {
164
+ if (error instanceof SyntaxError) {
165
+ this.error('--runtime-config must be valid JSON: ' + error.message);
166
+ }
167
+ throw error;
168
+ }
169
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
170
+ this.error('--runtime-config must be a JSON object');
171
+ }
172
+ const cfg = parsed;
173
+ const allowedKeys = new Set(['memory', 'timeout']);
174
+ for (const key of Object.keys(cfg)) {
175
+ if (!allowedKeys.has(key)) {
176
+ this.error(`--runtime-config has unknown field "${key}" (allowed: timeout, memory)`);
177
+ }
178
+ }
179
+ if (cfg.timeout === undefined && cfg.memory === undefined) {
180
+ this.error('--runtime-config must include at least one of "timeout" or "memory"');
181
+ }
182
+ const out = {};
183
+ if (cfg.timeout !== undefined) {
184
+ if (typeof cfg.timeout !== 'number' || !Number.isInteger(cfg.timeout)) {
185
+ this.error('--runtime-config "timeout" must be an integer (seconds)');
186
+ }
187
+ if (cfg.timeout < 1 || cfg.timeout > 900) {
188
+ this.error('--runtime-config "timeout" must be between 1 and 900 seconds (Lambda hard limit)');
189
+ }
190
+ out.timeout = cfg.timeout;
191
+ }
192
+ if (cfg.memory !== undefined) {
193
+ if (typeof cfg.memory !== 'number' || !Number.isInteger(cfg.memory)) {
194
+ this.error('--runtime-config "memory" must be an integer (MB)');
195
+ }
196
+ if (cfg.memory < 128 || cfg.memory > 10_240) {
197
+ this.error('--runtime-config "memory" must be between 128 and 10240 MB (Lambda hard limit)');
198
+ }
199
+ out.memory = cfg.memory;
200
+ }
201
+ mappedFields.runtimeConfig = out;
202
+ delete mappedFields['runtime-config'];
203
+ }
204
+ // Parse and validate dockerBuild JSON
205
+ if (mappedFields.dockerBuild !== undefined) {
206
+ try {
207
+ const parsed = JSON.parse(mappedFields.dockerBuild);
208
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
209
+ this.error('--dockerBuild must be a JSON object');
210
+ }
211
+ if (!parsed.image || !parsed.buildCommand || !parsed.artifactPaths || !parsed.artifactKey) {
212
+ this.error('--dockerBuild must include: image, buildCommand, artifactPaths, artifactKey');
213
+ }
214
+ mappedFields.dockerBuild = parsed;
215
+ }
216
+ catch (error) {
217
+ if (error instanceof SyntaxError) {
218
+ this.error('--dockerBuild must be valid JSON: ' + error.message);
219
+ }
220
+ throw error;
221
+ }
222
+ }
223
+ // Parse and validate dependsOn JSON
224
+ if (mappedFields.dependsOn !== undefined) {
225
+ try {
226
+ const parsed = JSON.parse(mappedFields.dependsOn);
227
+ if (!Array.isArray(parsed)) {
228
+ this.error('--dependsOn must be a JSON array');
229
+ }
230
+ for (const entry of parsed) {
231
+ if (typeof entry.artifactKey !== 'string' || !entry.artifactKey) {
232
+ this.error('Each dependsOn entry must have a non-empty "artifactKey" string');
233
+ }
234
+ if (typeof entry.downloadTo !== 'string' || !entry.downloadTo) {
235
+ this.error('Each dependsOn entry must have a non-empty "downloadTo" string');
236
+ }
237
+ }
238
+ mappedFields.dependsOn = parsed;
239
+ }
240
+ catch (error) {
241
+ if (error instanceof SyntaxError) {
242
+ this.error('--dependsOn must be valid JSON: ' + error.message);
243
+ }
244
+ throw error;
245
+ }
246
+ }
84
247
  // Filter out undefined values
85
- const updateData = Object.fromEntries(Object.entries(updateFields).filter(([_, value]) => value !== undefined));
248
+ const updateData = Object.fromEntries(Object.entries(mappedFields).filter(([_, value]) => value !== undefined));
86
249
  if (Object.keys(updateData).length === 0) {
87
250
  this.log(chalk.yellow('No fields to update. Please provide at least one field to update.'));
88
251
  return;
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class NetworkDiscover extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ account: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ region: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,210 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../utils/table.js';
6
+ export default class NetworkDiscover extends Command {
7
+ static description = 'Discover VPCs in a connected AWS account';
8
+ static examples = [
9
+ '<%= config.bin %> network discover --account 084309335408 --region us-east-1',
10
+ '<%= config.bin %> network discover --account 084309335408 --region us-east-1 --json',
11
+ ];
12
+ static flags = {
13
+ account: Flags.string({
14
+ char: 'a',
15
+ description: 'AWS Account ID to discover VPCs in',
16
+ }),
17
+ domain: Flags.string({
18
+ char: 'd',
19
+ description: 'Tenant domain (for multi-domain setups)',
20
+ }),
21
+ json: Flags.boolean({
22
+ description: 'Output raw JSON response (non-interactive)',
23
+ default: false,
24
+ }),
25
+ region: Flags.string({
26
+ char: 'r',
27
+ description: 'AWS region to discover VPCs in',
28
+ }),
29
+ };
30
+ async run() {
31
+ const { flags } = await this.parse(NetworkDiscover);
32
+ const service = new HyperdriveSigV4Service(flags.domain);
33
+ // Prompt for missing flags
34
+ let accountId = flags.account;
35
+ let region = flags.region;
36
+ if (!accountId || !region) {
37
+ const inquirer = (await import('inquirer')).default;
38
+ if (!accountId) {
39
+ // Try to fetch registered accounts for selection
40
+ try {
41
+ const accounts = await service.accountList();
42
+ if (accounts.length > 0) {
43
+ const { selectedAccount } = await inquirer.prompt([{
44
+ choices: accounts.map(a => ({
45
+ name: `${a.name || 'Unnamed'} (${a.accountId}) - ${a.defaultRegion}`,
46
+ value: a.accountId,
47
+ })),
48
+ message: chalk.yellow('Select an AWS account:'),
49
+ name: 'selectedAccount',
50
+ type: 'list',
51
+ }]);
52
+ accountId = selectedAccount;
53
+ // Default region from selected account
54
+ if (!region) {
55
+ const selectedAccountObj = accounts.find(a => a.accountId === selectedAccount);
56
+ region = selectedAccountObj?.defaultRegion;
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Fall through to manual input
62
+ }
63
+ if (!accountId) {
64
+ const { inputAccount } = await inquirer.prompt([{
65
+ message: chalk.yellow('Enter the AWS Account ID (12 digits):'),
66
+ name: 'inputAccount',
67
+ validate: (input) => /^\d{12}$/.test(input) || 'AWS Account ID must be exactly 12 digits',
68
+ }]);
69
+ accountId = inputAccount;
70
+ }
71
+ }
72
+ if (!region) {
73
+ const inquirerModule = (await import('inquirer')).default;
74
+ const { inputRegion } = await inquirerModule.prompt([{
75
+ choices: [
76
+ 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
77
+ 'sa-east-1', 'eu-west-1', 'eu-west-2', 'eu-central-1',
78
+ 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
79
+ ],
80
+ default: 'us-east-1',
81
+ message: chalk.yellow('Select the AWS region:'),
82
+ name: 'inputRegion',
83
+ type: 'list',
84
+ }]);
85
+ region = inputRegion;
86
+ }
87
+ }
88
+ const spinner = ora(`Discovering VPCs in ${accountId}/${region}...`).start();
89
+ try {
90
+ const result = await service.networkDiscover({
91
+ accountId: accountId,
92
+ region: region,
93
+ });
94
+ spinner.stop();
95
+ // JSON mode — output raw response and exit
96
+ if (flags.json) {
97
+ this.log(JSON.stringify(result, null, 2));
98
+ return;
99
+ }
100
+ if (result.vpcs.length === 0) {
101
+ this.log(chalk.yellow(`\nNo VPCs found in ${accountId}/${region}.`));
102
+ return;
103
+ }
104
+ this.log(chalk.blue(`\nDiscovered ${result.vpcs.length} VPC(s) in ${accountId}/${region}:\n`));
105
+ printTable(result.vpcs, {
106
+ vpcId: {
107
+ header: 'VPC ID',
108
+ minWidth: 25,
109
+ get: (row) => chalk.cyan(row.vpcId),
110
+ },
111
+ name: {
112
+ header: 'Name',
113
+ minWidth: 20,
114
+ get: (row) => row.name || chalk.gray('(unnamed)'),
115
+ },
116
+ cidrBlock: {
117
+ header: 'CIDR',
118
+ minWidth: 18,
119
+ },
120
+ subnets: {
121
+ header: 'Subnets (Priv/Pub)',
122
+ minWidth: 20,
123
+ get: (row) => `${row.privateSubnetIds.length} / ${row.publicSubnetIds.length}`,
124
+ },
125
+ sgs: {
126
+ header: 'Security Groups',
127
+ minWidth: 16,
128
+ get: (row) => String(row.securityGroups.length),
129
+ },
130
+ registered: {
131
+ header: 'Registered?',
132
+ get: (row) => row.alreadyRegistered
133
+ ? chalk.green(`Yes (${row.existingNetworkSlug})`)
134
+ : chalk.gray('No'),
135
+ },
136
+ }, (msg) => this.log(msg));
137
+ // Interactive mode — select VPCs to register
138
+ const unregisteredVpcs = result.vpcs.filter(v => !v.alreadyRegistered);
139
+ if (unregisteredVpcs.length === 0) {
140
+ this.log(chalk.green('\nAll discovered VPCs are already registered.'));
141
+ return;
142
+ }
143
+ const inquirer = (await import('inquirer')).default;
144
+ const { selectedVpcs } = await inquirer.prompt([{
145
+ choices: unregisteredVpcs.map(v => ({
146
+ name: `${v.vpcId} - ${v.name || '(unnamed)'} (${v.cidrBlock})`,
147
+ value: v.vpcId,
148
+ })),
149
+ message: chalk.yellow('\nSelect VPCs to register as networks:'),
150
+ name: 'selectedVpcs',
151
+ type: 'checkbox',
152
+ }]);
153
+ if (selectedVpcs.length === 0) {
154
+ this.log(chalk.gray('\nNo VPCs selected. Done.'));
155
+ return;
156
+ }
157
+ // Register each selected VPC
158
+ for (const vpcId of selectedVpcs) {
159
+ const vpc = unregisteredVpcs.find(v => v.vpcId === vpcId);
160
+ // Auto-generate slug from name tag or VPC ID suffix
161
+ const slugBase = vpc.name
162
+ ? vpc.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
163
+ : `vpc-${vpcId.slice(-8)}`;
164
+ const slug = `${slugBase}-${region}`;
165
+ const { confirmedSlug } = await inquirer.prompt([{
166
+ default: slug,
167
+ message: chalk.yellow(`Slug for ${vpcId}:`),
168
+ name: 'confirmedSlug',
169
+ validate: (input) => /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(input) || 'slug must be lowercase alphanumeric with hyphens',
170
+ }]);
171
+ const registerSpinner = ora(`Registering ${confirmedSlug}...`).start();
172
+ try {
173
+ await service.networkRegister({
174
+ slug: confirmedSlug,
175
+ accountId: accountId,
176
+ region: region,
177
+ vpcId,
178
+ privateSubnetIds: vpc.privateSubnetIds,
179
+ publicSubnetIds: vpc.publicSubnetIds,
180
+ source: 'discovered',
181
+ });
182
+ registerSpinner.succeed(chalk.green(`Registered: ${confirmedSlug}`));
183
+ }
184
+ catch (error) {
185
+ const axiosErr = error;
186
+ const errMsg = axiosErr.response?.data?.message ?? axiosErr.message ?? 'Unknown error';
187
+ registerSpinner.fail(chalk.red(`Failed to register ${confirmedSlug}: ${errMsg}`));
188
+ }
189
+ }
190
+ this.log(chalk.green('\nDone.'));
191
+ }
192
+ catch (error) {
193
+ spinner.stop();
194
+ const axiosError = error;
195
+ const statusCode = axiosError.response?.status;
196
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
197
+ if (statusCode === 403) {
198
+ this.log(chalk.red(`\n❌ Access denied: ${errorMessage}`));
199
+ this.log(chalk.gray('Ensure HyperdriveDiscoveryRole exists in the target account and allows this account to assume it.'));
200
+ }
201
+ else if (statusCode === 502) {
202
+ this.log(chalk.red(`\n❌ Discovery failed: ${errorMessage}`));
203
+ }
204
+ else {
205
+ this.log(chalk.red(`\n❌ Error: ${errorMessage}`));
206
+ }
207
+ this.exit(1);
208
+ }
209
+ }
210
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class NetworkGet extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,90 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ import { printHeader } from '../../utils/table.js';
6
+ export default class NetworkGet extends Command {
7
+ static description = 'Get details of a registered network';
8
+ static examples = [
9
+ '<%= config.bin %> network get prod-us-east-1',
10
+ '<%= config.bin %> network get prod-us-east-1 --json',
11
+ ];
12
+ static args = {
13
+ slug: Args.string({
14
+ description: 'Network slug',
15
+ required: true,
16
+ }),
17
+ };
18
+ static flags = {
19
+ domain: Flags.string({
20
+ char: 'd',
21
+ description: 'Tenant domain (for multi-domain setups)',
22
+ }),
23
+ json: Flags.boolean({
24
+ description: 'Output raw JSON response',
25
+ default: false,
26
+ }),
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(NetworkGet);
30
+ const isJson = flags.json;
31
+ const service = new HyperdriveSigV4Service(flags.domain);
32
+ const spinner = isJson ? null : ora(`Fetching network "${args.slug}"...`).start();
33
+ try {
34
+ const network = await service.networkGet(args.slug);
35
+ spinner?.succeed('Network found');
36
+ if (isJson) {
37
+ this.log(JSON.stringify(network, null, 2));
38
+ return;
39
+ }
40
+ printHeader(`Network: ${network.slug}`, (msg) => this.log(msg));
41
+ this.log(` ${chalk.bold('Account ID:')} ${network.accountId}`);
42
+ this.log(` ${chalk.bold('Region:')} ${network.region}`);
43
+ this.log(` ${chalk.bold('VPC ID:')} ${chalk.cyan(network.vpcId)}`);
44
+ this.log(` ${chalk.bold('Status:')} ${network.status === 'active' ? chalk.green(network.status) : chalk.yellow(network.status)}`);
45
+ this.log(` ${chalk.bold('Source:')} ${network.source}`);
46
+ this.log(` ${chalk.bold('Created:')} ${new Date(network.createdAt).toLocaleString()}`);
47
+ this.log(` ${chalk.bold('Updated:')} ${new Date(network.updatedAt).toLocaleString()}`);
48
+ this.log(`\n ${chalk.bold('Private Subnets:')}`);
49
+ if (network.privateSubnetIds.length > 0) {
50
+ for (const subnet of network.privateSubnetIds) {
51
+ this.log(` - ${subnet}`);
52
+ }
53
+ }
54
+ else {
55
+ this.log(chalk.gray(' (none)'));
56
+ }
57
+ this.log(`\n ${chalk.bold('Public Subnets:')}`);
58
+ if (network.publicSubnetIds.length > 0) {
59
+ for (const subnet of network.publicSubnetIds) {
60
+ this.log(` - ${subnet}`);
61
+ }
62
+ }
63
+ else {
64
+ this.log(chalk.gray(' (none)'));
65
+ }
66
+ this.log(`\n ${chalk.bold('Security Groups:')}`);
67
+ const sgEntries = Object.entries(network.securityGroups || {});
68
+ if (sgEntries.length > 0) {
69
+ for (const [purpose, groupId] of sgEntries) {
70
+ this.log(` ${chalk.bold(purpose)}: ${groupId}`);
71
+ }
72
+ }
73
+ else {
74
+ this.log(chalk.gray(' (none)'));
75
+ }
76
+ }
77
+ catch (error) {
78
+ spinner?.fail('Failed');
79
+ const axiosError = error;
80
+ const status = axiosError.response?.status;
81
+ if (status === 404) {
82
+ this.log(chalk.red(`\n❌ Network not found: ${args.slug}`));
83
+ }
84
+ else {
85
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
86
+ }
87
+ this.exit(1);
88
+ }
89
+ }
90
+ }
@@ -1,17 +1,10 @@
1
1
  import { Command } from '@oclif/core';
2
- export default class Logout extends Command {
2
+ export default class NetworkList extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  };
8
9
  run(): Promise<void>;
9
- /**
10
- * Remove credentials for a specific domain
11
- */
12
- private logoutDomain;
13
- /**
14
- * Remove default domain credentials and all stored config
15
- */
16
- private logoutAll;
17
10
  }
@@ -0,0 +1,71 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../utils/table.js';
6
+ export default class NetworkList extends Command {
7
+ static description = 'List all registered networks';
8
+ static examples = [
9
+ '<%= config.bin %> network list',
10
+ '<%= config.bin %> network list --json',
11
+ ];
12
+ static flags = {
13
+ domain: Flags.string({
14
+ char: 'd',
15
+ description: 'Tenant domain (for multi-domain setups)',
16
+ }),
17
+ json: Flags.boolean({
18
+ description: 'Output raw JSON response',
19
+ default: false,
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(NetworkList);
24
+ const isJson = flags.json;
25
+ const service = new HyperdriveSigV4Service(flags.domain);
26
+ const spinner = isJson ? null : ora('Fetching networks...').start();
27
+ try {
28
+ const networks = await service.networkList();
29
+ spinner?.stop();
30
+ if (isJson) {
31
+ this.log(JSON.stringify(networks, null, 2));
32
+ return;
33
+ }
34
+ if (!networks || networks.length === 0) {
35
+ this.log(chalk.yellow('\nNo networks found.'));
36
+ this.log(chalk.gray('Use "hd network discover" or "hd network register" to add networks.'));
37
+ return;
38
+ }
39
+ this.log(chalk.green(`\n${networks.length} network(s) found:\n`));
40
+ printTable(networks, {
41
+ slug: {
42
+ header: 'Slug',
43
+ minWidth: 25,
44
+ get: (row) => chalk.cyan(row.slug),
45
+ },
46
+ accountId: {
47
+ header: 'Account ID',
48
+ minWidth: 15,
49
+ },
50
+ region: {
51
+ header: 'Region',
52
+ minWidth: 15,
53
+ },
54
+ vpcId: {
55
+ header: 'VPC ID',
56
+ minWidth: 25,
57
+ },
58
+ status: {
59
+ header: 'Status',
60
+ get: (row) => row.status === 'active' ? chalk.green(row.status) : chalk.yellow(row.status),
61
+ },
62
+ }, (msg) => this.log(msg));
63
+ }
64
+ catch (error) {
65
+ spinner?.fail('Failed to fetch networks');
66
+ const axiosError = error;
67
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
68
+ this.exit(1);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class NetworkRegister extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'account-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'private-subnets': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'public-subnets': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ region: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'security-groups': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ slug: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'vpc-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }