@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
@@ -0,0 +1,85 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
4
+ export default class StageDelete extends Command {
5
+ static description = 'Delete a stage (cleanup runs asynchronously via SQS)';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %> --name stress-test-19 -d ds.hyperdrivebot.dev',
8
+ ];
9
+ static flags = {
10
+ domain: Flags.string({
11
+ char: 'd',
12
+ description: 'Tenant domain',
13
+ }),
14
+ force: Flags.boolean({
15
+ char: 'f',
16
+ description: 'Ignored (kept for backward compatibility — cleanup is always async now)',
17
+ default: false,
18
+ hidden: true,
19
+ }),
20
+ name: Flags.string({
21
+ char: 'n',
22
+ description: 'Stage name to delete',
23
+ required: true,
24
+ }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(StageDelete);
28
+ const service = new HyperdriveSigV4Service(flags.domain);
29
+ this.log(chalk.red(`\u{1F5D1}\u{FE0F} Deleting stage "${flags.name}"...`));
30
+ this.log(chalk.yellow(' This will destroy all CloudFormation stacks for this stage.'));
31
+ if (flags.force) {
32
+ this.log(chalk.yellow(' --force: Skipping domain teardown.'));
33
+ }
34
+ try {
35
+ const result = await service.stageDelete(flags.name, flags.force);
36
+ this.log(chalk.green('\n\u2705 Stage deleted from DynamoDB'));
37
+ if ('cleanup' in result && result.cleanup === 'queued') {
38
+ // New async path — cleanup runs via SQS worker
39
+ this.log(chalk.blue('\nCleanup queued \u2014 resources will be cleaned up asynchronously.'));
40
+ this.log(chalk.dim(' CF stacks, log groups, domain resources, and access tuples will be removed by the cleanup worker.'));
41
+ }
42
+ else {
43
+ // Old sync path (backward compat during rollout)
44
+ const syncResult = result;
45
+ if (syncResult.domainTeardown === 'skipped') {
46
+ this.log(chalk.yellow('\n\u26A0\uFE0F Domain teardown was skipped (--force). CloudFront/Route53 resources may remain.'));
47
+ }
48
+ if (syncResult.stacks.stacksDeleted.length > 0) {
49
+ this.log(chalk.blue(`\n\u{1F4E6} ${syncResult.stacks.stacksDeleted.length} stacks queued for deletion:`));
50
+ for (const stack of syncResult.stacks.stacksDeleted) {
51
+ this.log(chalk.dim(` - ${stack}`));
52
+ }
53
+ this.log(chalk.yellow('\n\u23F3 Stack deletion is async \u2014 may take several minutes to complete.'));
54
+ }
55
+ else {
56
+ this.log(chalk.dim('\n No matching CloudFormation stacks found.'));
57
+ }
58
+ if (syncResult.stacks.errors.length > 0) {
59
+ this.log(chalk.red(`\n\u26A0\uFE0F ${syncResult.stacks.errors.length} stack error(s):`));
60
+ for (const err of syncResult.stacks.errors) {
61
+ this.log(chalk.red(` - ${err}`));
62
+ }
63
+ }
64
+ // Log group cleanup results
65
+ if (syncResult.logGroups) {
66
+ if (syncResult.logGroups.deleted > 0) {
67
+ this.log(chalk.blue(`\n\u{1F9F9} ${syncResult.logGroups.deleted} CloudWatch log groups deleted`));
68
+ }
69
+ else {
70
+ this.log(chalk.dim('\n No CloudWatch log groups found to clean up.'));
71
+ }
72
+ if (syncResult.logGroups.errors.length > 0) {
73
+ this.log(chalk.red(`\n\u26A0\uFE0F ${syncResult.logGroups.errors.length} log group error(s):`));
74
+ for (const err of syncResult.logGroups.errors) {
75
+ this.log(chalk.red(` - ${err}`));
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ catch (error) {
82
+ this.error(`Failed to delete stage: ${error.message}`);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class StageDeploy extends Command {
3
+ static args: {
4
+ stageName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static flags: {
8
+ module: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ retry: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ /**
15
+ * Single-module surgical deploy
16
+ */
17
+ private runSingleModuleDeploy;
18
+ /**
19
+ * Retry all failed/pending modules
20
+ */
21
+ private runRetryDeploy;
22
+ /**
23
+ * Handle API error responses
24
+ */
25
+ private handleApiError;
26
+ /**
27
+ * Build CloudWatch console URL
28
+ */
29
+ private buildConsoleUrl;
30
+ /**
31
+ * Stream mission logs in real-time (shared by --retry and --module)
32
+ */
33
+ private streamMissionLogs;
34
+ }
@@ -0,0 +1,294 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ import { CloudWatchLogTailer } from '../../services/log-tailer.js';
6
+ export default class StageDeploy extends Command {
7
+ static args = {
8
+ stageName: Args.string({ description: 'Stage slug (e.g., ss-11)', required: true }),
9
+ };
10
+ static description = 'Retry or surgically redeploy modules on a per-branch stage';
11
+ static flags = {
12
+ module: Flags.string({
13
+ char: 'm',
14
+ description: 'Surgically redeploy a single module after validating prior-wave dependencies',
15
+ exclusive: ['retry'],
16
+ }),
17
+ retry: Flags.boolean({
18
+ char: 'r',
19
+ default: false,
20
+ description: 'Retry failed and pending modules from the last mission',
21
+ exclusive: ['module'],
22
+ }),
23
+ verbose: Flags.boolean({
24
+ char: 'v',
25
+ default: false,
26
+ description: 'Show verbose log output',
27
+ }),
28
+ yes: Flags.boolean({
29
+ char: 'y',
30
+ default: false,
31
+ description: 'Skip confirmation prompt',
32
+ }),
33
+ };
34
+ async run() {
35
+ const { args, flags } = await this.parse(StageDeploy);
36
+ // Mutual exclusion belt-and-suspenders (oclif handles it, but exit cleanly)
37
+ if (flags.retry && flags.module) {
38
+ this.error('--retry and --module are mutually exclusive', { exit: 2 });
39
+ return;
40
+ }
41
+ if (!flags.retry && !flags.module) {
42
+ this.log(chalk.yellow('Please specify a deploy mode: --retry or --module <slug>'));
43
+ this.log(chalk.gray(' Examples:'));
44
+ this.log(chalk.gray(' hd stage deploy ss-11 --retry'));
45
+ this.log(chalk.gray(' hd stage deploy ss-11 --module sign'));
46
+ this.exit(1);
47
+ return;
48
+ }
49
+ try {
50
+ const service = new HyperdriveSigV4Service();
51
+ if (flags.module) {
52
+ await this.runSingleModuleDeploy(service, args.stageName, flags.module, flags.verbose, flags.yes);
53
+ }
54
+ else {
55
+ await this.runRetryDeploy(service, args.stageName, flags.verbose, flags.yes);
56
+ }
57
+ }
58
+ catch (error) {
59
+ const err = error;
60
+ // Ignore EEXIT with code 0
61
+ if (err.message?.includes('EEXIT') && err.oclif?.exit === 0) {
62
+ return;
63
+ }
64
+ // Preserve explicit non-zero oclif exits
65
+ if (err.oclif?.exit !== undefined && err.oclif.exit !== 0) {
66
+ throw err;
67
+ }
68
+ this.handleApiError(error, args.stageName);
69
+ }
70
+ }
71
+ /**
72
+ * Single-module surgical deploy
73
+ */
74
+ async runSingleModuleDeploy(service, stageName, moduleSlug, verbose, skipPrompt) {
75
+ // 1. Fetch manifest for confirmation
76
+ this.log(chalk.gray('Fetching deploy manifest...'));
77
+ const manifestResponse = await service.stageGetDeployManifest(stageName);
78
+ if (!manifestResponse.manifest) {
79
+ this.log(chalk.yellow(`No deploy manifest found for stage '${stageName}'. Run \`hd stage create\` first.`));
80
+ this.exit(0);
81
+ return;
82
+ }
83
+ const manifest = manifestResponse.manifest;
84
+ // 2. Find the module and its wave
85
+ let targetWave = -1;
86
+ let found = false;
87
+ for (const wave of manifest.waves) {
88
+ for (const mod of wave.modules) {
89
+ if (mod.moduleSlug === moduleSlug) {
90
+ targetWave = wave.waveIndex;
91
+ found = true;
92
+ break;
93
+ }
94
+ }
95
+ if (found)
96
+ break;
97
+ }
98
+ if (!found) {
99
+ this.log(chalk.red(`Module '${moduleSlug}' not found in manifest for stage ${stageName}`));
100
+ this.exit(2);
101
+ return;
102
+ }
103
+ // 3. Count deployed modules in prior waves
104
+ const priorDeployedCount = manifest.waves
105
+ .filter(w => w.waveIndex < targetWave)
106
+ .reduce((sum, w) => sum + w.modules.length, 0);
107
+ // 4. Confirmation prompt
108
+ if (!skipPrompt) {
109
+ this.log('');
110
+ this.log(chalk.blue(`Deploying 1 module: ${moduleSlug} (W${targetWave}). Prior-wave deps OK (${priorDeployedCount} modules deployed).`));
111
+ this.log('');
112
+ const { confirm } = await inquirer.prompt([{
113
+ default: true,
114
+ message: 'Continue? [Y/n]',
115
+ name: 'confirm',
116
+ type: 'confirm',
117
+ }]);
118
+ if (!confirm) {
119
+ this.log('Deploy cancelled.');
120
+ this.exit(0);
121
+ return;
122
+ }
123
+ }
124
+ // 5. Call the deploy API
125
+ this.log(chalk.gray(`Starting single-module deploy for ${moduleSlug}...`));
126
+ const response = await service.stageDeploy({
127
+ mode: 'module',
128
+ moduleSlug,
129
+ stageName,
130
+ });
131
+ // 6. Stream mission logs (reuse same helper as --retry and create --autoLaunch)
132
+ await this.streamMissionLogs(response.logging, response.missionId, verbose, 'module', moduleSlug);
133
+ }
134
+ /**
135
+ * Retry all failed/pending modules
136
+ */
137
+ async runRetryDeploy(service, stageName, verbose, skipPrompt) {
138
+ // 1. Fetch manifest to show confirmation prompt
139
+ this.log(chalk.gray('Fetching deploy manifest...'));
140
+ const manifestResponse = await service.stageGetDeployManifest(stageName);
141
+ if (!manifestResponse.manifest) {
142
+ this.log(chalk.yellow(`No deploy manifest found for stage '${stageName}'. Run \`hd stage create\` first.`));
143
+ this.exit(0);
144
+ return;
145
+ }
146
+ const manifest = manifestResponse.manifest;
147
+ // 2. Compute non-deployed modules
148
+ const retryableModules = [];
149
+ for (const wave of manifest.waves) {
150
+ for (const mod of wave.modules) {
151
+ if (mod.status !== 'deployed') {
152
+ retryableModules.push({
153
+ moduleSlug: mod.moduleSlug,
154
+ status: mod.status,
155
+ waveIndex: wave.waveIndex,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ if (retryableModules.length === 0) {
161
+ this.log(chalk.green('All modules are already deployed. Nothing to retry.'));
162
+ this.exit(0);
163
+ return;
164
+ }
165
+ // 3. Print retry summary and confirm
166
+ this.log('');
167
+ this.log(chalk.blue(`Retrying ${retryableModules.length} modules:`));
168
+ for (const mod of retryableModules) {
169
+ this.log(chalk.gray(` - ${mod.moduleSlug} (W${mod.waveIndex}, ${mod.status})`));
170
+ }
171
+ if (!skipPrompt) {
172
+ this.log('');
173
+ const { confirm } = await inquirer.prompt([{
174
+ default: true,
175
+ message: 'Continue?',
176
+ name: 'confirm',
177
+ type: 'confirm',
178
+ }]);
179
+ if (!confirm) {
180
+ this.log('Retry cancelled.');
181
+ this.exit(0);
182
+ return;
183
+ }
184
+ }
185
+ // 4. Call the deploy API
186
+ this.log(chalk.gray('Starting retry...'));
187
+ const response = await service.stageDeploy({
188
+ mode: 'retry',
189
+ stageName,
190
+ });
191
+ // 5. Stream mission logs
192
+ await this.streamMissionLogs(response.logging, response.missionId, verbose, 'retry');
193
+ }
194
+ /**
195
+ * Handle API error responses
196
+ */
197
+ handleApiError(error, stageName) {
198
+ const axiosError = error;
199
+ const responseData = axiosError.response?.data;
200
+ // The API returns structured errors as JSON-stringified message
201
+ let errorCode;
202
+ let errorMessage;
203
+ let parsedBody;
204
+ if (responseData?.message) {
205
+ try {
206
+ parsedBody = JSON.parse(responseData.message);
207
+ errorCode = parsedBody?.code;
208
+ errorMessage = parsedBody?.error ?? responseData.message;
209
+ }
210
+ catch {
211
+ errorMessage = responseData.message;
212
+ }
213
+ }
214
+ else {
215
+ errorMessage = responseData?.error ?? axiosError.message ?? 'Unknown error';
216
+ errorCode = responseData?.code;
217
+ parsedBody = responseData;
218
+ }
219
+ // NOTHING_TO_RETRY is not an error — clean exit
220
+ if (errorCode === 'NOTHING_TO_RETRY') {
221
+ this.log(chalk.green(errorMessage));
222
+ this.exit(0);
223
+ return;
224
+ }
225
+ // WAVE_DEPS_NOT_MET — format blocking modules
226
+ if (errorCode === 'WAVE_DEPS_NOT_MET' || (parsedBody?.error === 'WAVE_DEPS_NOT_MET')) {
227
+ const targetModule = parsedBody?.targetModule || 'unknown';
228
+ const targetWave = parsedBody?.targetWave ?? '?';
229
+ const blockers = parsedBody?.blockingModules || [];
230
+ const blockerList = blockers.map(b => `${b.slug} (W${b.wave}, ${b.status})`).join(', ');
231
+ this.log(chalk.red(`Cannot deploy ${targetModule} (W${targetWave}) — blocking modules: ${blockerList}. Run 'hd stage deploy ${stageName} --retry' first.`));
232
+ this.exit(2);
233
+ return;
234
+ }
235
+ // PREVIOUS_MISSION_NOT_TERMINAL
236
+ if (errorCode === 'PREVIOUS_MISSION_NOT_TERMINAL' || errorCode === 'MISSION_IN_FLIGHT') {
237
+ const missionId = parsedBody?.missionId || 'unknown';
238
+ const state = parsedBody?.state || 'RUNNING';
239
+ this.log(chalk.red(`Previous mission ${missionId} is still ${state}. Wait for it to complete before retrying.`));
240
+ this.exit(2);
241
+ return;
242
+ }
243
+ // MODULE_NOT_FOUND
244
+ if (errorCode === 'MODULE_NOT_FOUND') {
245
+ this.log(chalk.red(errorMessage));
246
+ this.exit(2);
247
+ return;
248
+ }
249
+ this.log(chalk.red(`❌ ${errorMessage}`));
250
+ this.exit(1);
251
+ }
252
+ /**
253
+ * Build CloudWatch console URL
254
+ */
255
+ buildConsoleUrl(config) {
256
+ const encodedGroup = encodeURIComponent(encodeURIComponent(config.logGroup));
257
+ const encodedStream = encodeURIComponent(encodeURIComponent(config.logStream));
258
+ return `https://${config.region}.console.aws.amazon.com/cloudwatch/home?region=${config.region}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`;
259
+ }
260
+ /**
261
+ * Stream mission logs in real-time (shared by --retry and --module)
262
+ */
263
+ async streamMissionLogs(loggingConfig, missionId, verbose, mode, moduleSlug) {
264
+ if (mode === 'module') {
265
+ this.log(chalk.blue(`\n🎯 Single-module deploy started: ${missionId}`));
266
+ this.log(chalk.gray(` Deploying: ${moduleSlug}`));
267
+ }
268
+ else {
269
+ this.log(chalk.blue(`\n🚀 Retry mission started: ${missionId}`));
270
+ this.log(chalk.gray(' Resuming deployment of failed/pending modules...'));
271
+ }
272
+ this.log('');
273
+ const tailer = new CloudWatchLogTailer(loggingConfig, {
274
+ pollInterval: 1000,
275
+ showDebug: false,
276
+ verbose,
277
+ });
278
+ const outcome = await tailer.tail();
279
+ this.log('');
280
+ if (outcome.success) {
281
+ this.log(chalk.green(`✅ ${outcome.message}`));
282
+ this.log(chalk.cyan(` Mission: ${missionId}`));
283
+ const consoleUrl = this.buildConsoleUrl(loggingConfig);
284
+ this.log(chalk.gray(` Logs: ${consoleUrl}`));
285
+ this.exit(0);
286
+ }
287
+ else {
288
+ this.log(chalk.red(`❌ ${outcome.message}`));
289
+ const consoleUrl = this.buildConsoleUrl(loggingConfig);
290
+ this.log(chalk.gray(` Full logs: ${consoleUrl}`));
291
+ this.exit(1);
292
+ }
293
+ }
294
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `hd stage ensure-branches <stageName>` — ensure the stage branch exists on
3
+ * all linked project remotes, creating it when missing.
4
+ *
5
+ * This is a CLI-side command: by the time `hd stage create --autoLaunch`
6
+ * kicks off the builder pods, branches must already exist on linked frontend
7
+ * remotes. Running this command beforehand is the primary fix for the
8
+ * "sign + tenant-seed blocked because the SS-N branch doesn't exist on the
9
+ * Sign remote" class of mission failures.
10
+ */
11
+ import { Command } from '@oclif/core';
12
+ export default class StageEnsureBranches extends Command {
13
+ static description: string;
14
+ static args: {
15
+ stageName: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
16
+ };
17
+ static flags: {
18
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ };
22
+ run(): Promise<void>;
23
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `hd stage ensure-branches <stageName>` — ensure the stage branch exists on
3
+ * all linked project remotes, creating it when missing.
4
+ *
5
+ * This is a CLI-side command: by the time `hd stage create --autoLaunch`
6
+ * kicks off the builder pods, branches must already exist on linked frontend
7
+ * remotes. Running this command beforehand is the primary fix for the
8
+ * "sign + tenant-seed blocked because the SS-N branch doesn't exist on the
9
+ * Sign remote" class of mission failures.
10
+ */
11
+ import { Args, Command, Flags } from '@oclif/core';
12
+ import chalk from 'chalk';
13
+ import { ensureBranches } from '../../lib/ensure-branches.js';
14
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
15
+ export default class StageEnsureBranches extends Command {
16
+ static description = 'Ensure the stage branch exists on all linked project remotes (creates if missing)';
17
+ static args = {
18
+ stageName: Args.string({ description: 'Name of the stage', required: true }),
19
+ };
20
+ static flags = {
21
+ verbose: Flags.boolean({
22
+ char: 'v',
23
+ default: false,
24
+ description: 'Print per-project status',
25
+ }),
26
+ dryRun: Flags.boolean({
27
+ default: false,
28
+ description: 'Detect missing branches without creating them',
29
+ }),
30
+ domain: Flags.string({
31
+ char: 'd',
32
+ description: 'Tenant domain (for multi-domain setups)',
33
+ }),
34
+ };
35
+ async run() {
36
+ const { args, flags } = await this.parse(StageEnsureBranches);
37
+ try {
38
+ const service = new HyperdriveSigV4Service(flags.domain);
39
+ this.log(chalk.blue(`🌿 Ensuring branches for stage: ${args.stageName}`));
40
+ const stage = await service.stageGet({ name: args.stageName });
41
+ if (!stage.branchName) {
42
+ this.error('Stage has no branchName — cannot ensure branches');
43
+ }
44
+ // Prefer `projectSlugs` (creation-time intent list) — fall back to the
45
+ // keys of `projects` (runtime tracking map) if the backend only
46
+ // exposes the deployment state.
47
+ const projectSlugs = (stage.projectSlugs && stage.projectSlugs.length > 0
48
+ ? stage.projectSlugs
49
+ : stage.projects
50
+ ? Object.keys(stage.projects)
51
+ : []);
52
+ if (projectSlugs.length === 0) {
53
+ this.log(chalk.gray(`ℹ️ No linked projects for stage ${args.stageName} — nothing to do`));
54
+ this.exit(0);
55
+ }
56
+ const allModules = await service.moduleList();
57
+ const linkedProjects = allModules
58
+ .filter((m) => m.slug && projectSlugs.includes(m.slug))
59
+ .map((m) => ({
60
+ slug: m.slug,
61
+ gitProvider: m.gitProvider,
62
+ gitFullRepoPath: m.gitFullRepoPath,
63
+ }));
64
+ if (flags.dryRun) {
65
+ this.log(chalk.yellow('[dry-run] detection only — no branches will be created'));
66
+ }
67
+ const result = await ensureBranches({
68
+ stageName: args.stageName,
69
+ branchName: stage.branchName,
70
+ linkedProjects,
71
+ verbose: flags.verbose,
72
+ dryRun: flags.dryRun,
73
+ logger: (msg) => this.log(msg),
74
+ });
75
+ // Always print a one-line summary (even in --verbose mode).
76
+ const summary = `${result.created + result.alreadyExisted}/${result.total} branches ensured (${result.created} created, ${result.alreadyExisted} already existed, ${result.failed} failed)`;
77
+ if (result.failed > 0) {
78
+ this.log(chalk.red(`❌ ${summary}`));
79
+ this.exit(1);
80
+ }
81
+ else {
82
+ this.log(chalk.green(`✅ ${summary}`));
83
+ }
84
+ }
85
+ catch (error) {
86
+ const err = error;
87
+ // Ignore EEXIT with code 0 — oclif's clean-exit pattern.
88
+ if (err.message?.includes('EEXIT') && err.oclif?.exit === 0) {
89
+ return;
90
+ }
91
+ // Re-throw oclif exits with non-zero code so the process bubbles up.
92
+ if (err.oclif?.exit !== undefined && err.oclif.exit !== 0) {
93
+ throw err;
94
+ }
95
+ const axiosError = error;
96
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
97
+ this.log(chalk.red(`❌ Error ensuring branches: ${errorMessage}`));
98
+ this.exit(1);
99
+ }
100
+ }
101
+ }
@@ -46,6 +46,10 @@ export default class StageList extends Command {
46
46
  header: 'Regions',
47
47
  get: (row) => (row.regions || []).join(', '),
48
48
  },
49
+ customDomain: {
50
+ header: 'Domain',
51
+ get: (row) => row.customDomain || '',
52
+ },
49
53
  defaultStage: {
50
54
  header: 'Default',
51
55
  get: (row) => row.defaultStage ? chalk.green('✓') : '',
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class StageStatus extends Command {
3
+ static args: {
4
+ slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ private renderManifest;
14
+ }
@@ -0,0 +1,100 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
4
+ const STATUS_ICONS = {
5
+ deployed: '✅',
6
+ failed: '❌',
7
+ skipped: '⏸️',
8
+ pending: '⏳',
9
+ timeout: '⏱️',
10
+ };
11
+ const colorizeStatus = (status) => {
12
+ switch (status) {
13
+ case 'success':
14
+ return chalk.green(status);
15
+ case 'running':
16
+ case 'partial-failure':
17
+ return chalk.yellow(status);
18
+ case 'failed':
19
+ return chalk.red(status);
20
+ default:
21
+ return status;
22
+ }
23
+ };
24
+ export default class StageStatus extends Command {
25
+ static args = {
26
+ slug: Args.string({ description: 'Stage slug (e.g. ss-11)', required: true }),
27
+ };
28
+ static description = 'Show deployment status for a stage';
29
+ static examples = [
30
+ '<%= config.bin %> <%= command.id %> ss-11',
31
+ '<%= config.bin %> <%= command.id %> ss-11 --verbose',
32
+ ];
33
+ static flags = {
34
+ domain: Flags.string({
35
+ char: 'd',
36
+ description: 'Tenant domain (for multi-domain setups)',
37
+ }),
38
+ verbose: Flags.boolean({
39
+ char: 'v',
40
+ description: 'Show deploymentId and errorType per module',
41
+ }),
42
+ };
43
+ async run() {
44
+ const { args, flags } = await this.parse(StageStatus);
45
+ try {
46
+ const service = new HyperdriveSigV4Service(flags.domain);
47
+ const response = await service.stageGetDeployManifest(args.slug);
48
+ if (!response.manifest) {
49
+ this.log(chalk.yellow(`⚠️ No deployment data for stage ${args.slug}`));
50
+ this.log(chalk.gray(`The stage exists but has no deploy manifest. Run hd stage create ${args.slug} --autoLaunch to deploy it.`));
51
+ return;
52
+ }
53
+ this.renderManifest(response.manifest, args.slug, flags.verbose ?? false);
54
+ }
55
+ catch (error) {
56
+ const statusCode = error?.response?.status ?? error?.statusCode;
57
+ if (statusCode === 404) {
58
+ this.error(chalk.red(`❌ Stage ${args.slug} not found`));
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ renderManifest(manifest, slug, verbose) {
64
+ // Compute totals
65
+ const allModules = manifest.waves.flatMap(w => w.modules);
66
+ const total = allModules.length;
67
+ const counts = { deployed: 0, failed: 0, pending: 0, skipped: 0, timeout: 0 };
68
+ for (const m of allModules) {
69
+ counts[m.status] = (counts[m.status] || 0) + 1;
70
+ }
71
+ // Header
72
+ this.log(`Stage: ${chalk.cyan(slug)} | Mission: ${chalk.cyan(manifest.missionId)} | Status: ${colorizeStatus(manifest.status)}`);
73
+ this.log(`Started: ${manifest.startedAt} Updated: ${manifest.updatedAt} ` +
74
+ `Modules: ${counts.deployed}/${total} ✅ ${counts.failed} ❌ ${counts.skipped} ⏸️ ${counts.pending} ⏳ ${counts.timeout} ⏱️`);
75
+ this.log('');
76
+ // Waves
77
+ for (const wave of manifest.waves) {
78
+ const waveDeployed = wave.modules.filter(m => m.status === 'deployed').length;
79
+ if (verbose) {
80
+ this.log(`W${wave.waveIndex}: (${waveDeployed}/${wave.modules.length})`);
81
+ for (const m of wave.modules) {
82
+ const icon = STATUS_ICONS[m.status] || '?';
83
+ let line = ` ${m.moduleSlug} ${icon} ${m.status}`;
84
+ if (m.deploymentId)
85
+ line += ` deploymentId=${m.deploymentId}`;
86
+ if (m.errorType)
87
+ line += ` errorType=${m.errorType}`;
88
+ this.log(line);
89
+ }
90
+ }
91
+ else {
92
+ const moduleSegments = wave.modules.map(m => {
93
+ const icon = STATUS_ICONS[m.status] || '?';
94
+ return `[${m.moduleSlug} ${icon}]`;
95
+ });
96
+ this.log(`W${wave.waveIndex}: ${moduleSegments.join(' ')} (${waveDeployed}/${wave.modules.length})`);
97
+ }
98
+ }
99
+ }
100
+ }