@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,173 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { Args, Command, Flags } from '@oclif/core';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
7
+ import { pollLifecycle } from '../../utils/lifecycle-poller.js';
8
+ export default class ServiceSeed extends Command {
9
+ static description = 'Execute a SQL script against a service database';
10
+ static examples = [
11
+ '<%= config.bin %> service seed rds-postgres-live --from ./seed.sql',
12
+ '<%= config.bin %> service seed rds-postgres-live --inline "INSERT INTO users VALUES (1, \'admin\')"',
13
+ '<%= config.bin %> service seed rds-postgres-live --from ./seed.sql --allow-production',
14
+ '<%= config.bin %> service seed rds-postgres-live --from ./seed.sql --no-wait --json',
15
+ ];
16
+ static args = {
17
+ slug: Args.string({
18
+ description: 'Service slug to seed',
19
+ required: true,
20
+ }),
21
+ };
22
+ static flags = {
23
+ domain: Flags.string({
24
+ char: 'd',
25
+ description: 'Tenant domain (for multi-domain setups)',
26
+ }),
27
+ from: Flags.string({
28
+ description: 'Path to local SQL script file to upload and execute',
29
+ exclusive: ['inline'],
30
+ }),
31
+ inline: Flags.string({
32
+ description: 'Inline SQL string to execute directly',
33
+ exclusive: ['from'],
34
+ }),
35
+ 'allow-production': Flags.boolean({
36
+ description: 'Allow seeding production-bound services',
37
+ default: false,
38
+ }),
39
+ json: Flags.boolean({
40
+ description: 'Output raw JSON response',
41
+ default: false,
42
+ }),
43
+ 'no-wait': Flags.boolean({
44
+ description: 'Do not wait for seed to complete',
45
+ default: false,
46
+ }),
47
+ };
48
+ async run() {
49
+ const { args, flags } = await this.parse(ServiceSeed);
50
+ const service = new HyperdriveSigV4Service(flags.domain);
51
+ const isJson = flags.json;
52
+ // Validate exactly one of --from or --inline is provided
53
+ if (!flags.from && !flags.inline) {
54
+ this.log(chalk.red('Error: Provide either --from <file> or --inline <sql>'));
55
+ this.exit(1);
56
+ return;
57
+ }
58
+ let scriptSource;
59
+ let scriptPath;
60
+ if (flags.from) {
61
+ try {
62
+ scriptSource = readFileSync(flags.from, 'utf-8');
63
+ scriptPath = basename(flags.from);
64
+ }
65
+ catch (error) {
66
+ this.log(chalk.red(`Error reading file: ${error.message}`));
67
+ this.exit(1);
68
+ return;
69
+ }
70
+ }
71
+ else {
72
+ scriptSource = flags.inline;
73
+ }
74
+ // Production safety confirmation
75
+ if (flags['allow-production'] && !isJson) {
76
+ this.log(chalk.red.bold('\n⚠️ WARNING: You are about to seed a production-bound service.'));
77
+ this.log(chalk.red(` Service: ${args.slug}\n`));
78
+ const response = await new Promise((resolve) => {
79
+ process.stdout.write(chalk.yellow('Are you sure you want to proceed? (y/N): '));
80
+ process.stdin.setEncoding('utf-8');
81
+ process.stdin.once('data', (data) => {
82
+ resolve(data.toString().trim().toLowerCase());
83
+ });
84
+ });
85
+ if (response !== 'y' && response !== 'yes') {
86
+ this.log(chalk.gray('Aborted.'));
87
+ return;
88
+ }
89
+ }
90
+ const spinner = isJson ? null : ora('Submitting seed request...').start();
91
+ try {
92
+ const seedRun = await service.serviceSeed(args.slug, {
93
+ scriptSource,
94
+ scriptPath,
95
+ allowProduction: flags['allow-production'] || undefined,
96
+ });
97
+ if (isJson && flags['no-wait']) {
98
+ this.log(JSON.stringify(seedRun, null, 2));
99
+ return;
100
+ }
101
+ spinner?.succeed(chalk.green(`Seed job submitted: ${seedRun.id}`));
102
+ if (!isJson) {
103
+ this.log(` Service: ${chalk.cyan(args.slug)}`);
104
+ this.log(` Seed Run ID: ${chalk.cyan(seedRun.id)}`);
105
+ this.log(` Status: ${chalk.yellow(seedRun.status)}`);
106
+ if (seedRun.logGroupName) {
107
+ this.log(` Log Group: ${chalk.gray(seedRun.logGroupName)}`);
108
+ }
109
+ this.log('');
110
+ }
111
+ if (flags['no-wait']) {
112
+ if (!isJson) {
113
+ this.log(chalk.gray(`Check status: hd service backups ${args.slug}`));
114
+ }
115
+ return;
116
+ }
117
+ // Poll for seed completion
118
+ const result = await pollLifecycle({
119
+ pollFn: () => service.serviceGetSeedRun(args.slug, seedRun.id),
120
+ getStatus: (sr) => sr.status,
121
+ terminalStates: new Set(['completed', 'failed']),
122
+ successStates: new Set(['completed']),
123
+ getErrorMessage: (sr) => sr.errorMessage,
124
+ operationLabel: 'Seeding',
125
+ }, isJson);
126
+ if (isJson) {
127
+ this.log(JSON.stringify(result.entity, null, 2));
128
+ }
129
+ if (result.timedOut) {
130
+ if (!isJson) {
131
+ this.log(chalk.yellow('The seed job may still be running.'));
132
+ this.log(chalk.gray(`Check status: hd service backups ${args.slug}`));
133
+ }
134
+ this.exit(1);
135
+ }
136
+ if (!result.success) {
137
+ this.exit(1);
138
+ }
139
+ // Success summary
140
+ if (!isJson && result.entity) {
141
+ if (result.entity.logGroupName) {
142
+ this.log(chalk.gray(` Log group: ${result.entity.logGroupName}`));
143
+ }
144
+ if (result.entity.completedAt) {
145
+ this.log(` Completed: ${new Date(result.entity.completedAt).toLocaleString()}`);
146
+ }
147
+ }
148
+ }
149
+ catch (error) {
150
+ spinner?.fail('Seed request failed');
151
+ const axiosError = error;
152
+ const status = axiosError.response?.status;
153
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
154
+ if (status === 400) {
155
+ this.log(chalk.red(`Validation error: ${errorMessage}`));
156
+ }
157
+ else if (status === 403) {
158
+ this.log(chalk.red(`Production safety: ${errorMessage}`));
159
+ this.log(chalk.yellow('Use --allow-production flag to override.'));
160
+ }
161
+ else if (status === 404) {
162
+ this.log(chalk.red(`Not found: ${errorMessage}`));
163
+ }
164
+ else if (status === 409) {
165
+ this.log(chalk.red(`Conflict: ${errorMessage}`));
166
+ }
167
+ else {
168
+ this.log(chalk.red(`Error: ${errorMessage}`));
169
+ }
170
+ this.exit(1);
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceTemplates extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,66 @@
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 ServiceTemplates extends Command {
7
+ static description = 'List available service provisioning templates';
8
+ static examples = [
9
+ '<%= config.bin %> service templates',
10
+ '<%= config.bin %> service templates --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(ServiceTemplates);
24
+ const isJson = flags.json;
25
+ const service = new HyperdriveSigV4Service(flags.domain);
26
+ const spinner = isJson ? null : ora('Fetching service templates...').start();
27
+ try {
28
+ const templates = await service.serviceTemplates();
29
+ spinner?.stop();
30
+ if (isJson) {
31
+ this.log(JSON.stringify(templates, null, 2));
32
+ return;
33
+ }
34
+ if (!templates || templates.length === 0) {
35
+ this.log(chalk.yellow('\nNo service templates available.'));
36
+ return;
37
+ }
38
+ this.log(chalk.green(`\n${templates.length} template(s) available:\n`));
39
+ printTable(templates, {
40
+ id: {
41
+ header: 'ID',
42
+ minWidth: 25,
43
+ get: (row) => chalk.cyan(row.id),
44
+ },
45
+ name: {
46
+ header: 'Name',
47
+ minWidth: 25,
48
+ },
49
+ type: {
50
+ header: 'Type',
51
+ minWidth: 18,
52
+ },
53
+ description: {
54
+ header: 'Description',
55
+ minWidth: 30,
56
+ },
57
+ }, (msg) => this.log(msg));
58
+ }
59
+ catch (error) {
60
+ spinner?.fail('Failed to fetch templates');
61
+ const axiosError = error;
62
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
63
+ this.exit(1);
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceUnbind extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ service: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ module: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ stage: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,74 @@
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
+ export default class ServiceUnbind extends Command {
6
+ static description = 'Unbind a service from a module+stage';
7
+ static examples = [
8
+ '<%= config.bin %> service unbind rds-postgres-live my-api --stage live',
9
+ '<%= config.bin %> service unbind rds-postgres-live my-api --stage live --yes',
10
+ ];
11
+ static args = {
12
+ service: Args.string({
13
+ description: 'Service slug',
14
+ required: true,
15
+ }),
16
+ module: Args.string({
17
+ description: 'Module slug',
18
+ required: true,
19
+ }),
20
+ };
21
+ static flags = {
22
+ domain: Flags.string({
23
+ char: 'd',
24
+ description: 'Tenant domain (for multi-domain setups)',
25
+ }),
26
+ stage: Flags.string({
27
+ description: 'Deployment stage',
28
+ required: true,
29
+ }),
30
+ yes: Flags.boolean({
31
+ char: 'y',
32
+ description: 'Skip confirmation prompt',
33
+ default: false,
34
+ }),
35
+ };
36
+ async run() {
37
+ const { args, flags } = await this.parse(ServiceUnbind);
38
+ const service = new HyperdriveSigV4Service(flags.domain);
39
+ // Confirmation prompt unless --yes
40
+ if (!flags.yes) {
41
+ const inquirer = (await import('inquirer')).default;
42
+ const { confirmed } = await inquirer.prompt([{
43
+ default: false,
44
+ message: chalk.yellow(`Unbind "${args.service}" from "${args.module}" (stage: ${flags.stage})?`),
45
+ name: 'confirmed',
46
+ type: 'confirm',
47
+ }]);
48
+ if (!confirmed) {
49
+ this.log(chalk.gray('Cancelled.'));
50
+ return;
51
+ }
52
+ }
53
+ const spinner = ora(`Unbinding "${args.service}" from "${args.module}" (stage: ${flags.stage})...`).start();
54
+ try {
55
+ await service.serviceUnbind(args.service, {
56
+ moduleSlug: args.module,
57
+ stage: flags.stage,
58
+ });
59
+ spinner.succeed(chalk.green(`Service unbound: ${args.service} ✕ ${args.module} (${flags.stage})`));
60
+ }
61
+ catch (error) {
62
+ spinner.fail('Unbind failed');
63
+ const axiosError = error;
64
+ const status = axiosError.response?.status;
65
+ if (status === 404) {
66
+ this.log(chalk.red(`\n❌ Binding not found: ${args.service} → ${args.module} (${flags.stage})`));
67
+ }
68
+ else {
69
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
70
+ }
71
+ this.exit(1);
72
+ }
73
+ }
74
+ }
@@ -3,7 +3,9 @@ export default class StageCreate extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
5
  accountId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ autoCreateBranches: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
7
  autoLaunch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ autoWarm: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
9
  branchName: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  defaultStage: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
11
  deletionProtection: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -13,16 +15,37 @@ export default class StageCreate extends Command {
13
15
  project: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
16
  provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
17
  region: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ warmConcurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
16
19
  };
17
20
  run(): Promise<void>;
18
21
  /**
19
22
  * Build CloudWatch console URL
20
23
  */
21
24
  private buildConsoleUrl;
25
+ /**
26
+ * Ensure the stage branch exists on every linked project remote BEFORE
27
+ * `service.stageCreate` is invoked. Implements the contract described in
28
+ * Story 1.2 ACs 3–5 + 9:
29
+ *
30
+ * - all projects succeed → proceed silently to stageCreate
31
+ * - some projects fail (partial) → log yellow warning + proceed
32
+ * - every project fails → log red error + exit(1) + NEVER
33
+ * call stageCreate
34
+ *
35
+ * The shared `ensureBranches()` lib never throws — all errors are collected
36
+ * into `result.failed`, so we translate the result into the three branches
37
+ * above here.
38
+ */
39
+ private runEnsureBranches;
22
40
  private getGitBranch;
23
41
  private promptUser;
24
42
  /**
25
43
  * Stream mission logs in real-time
26
44
  */
27
45
  private streamMissionLogs;
46
+ /**
47
+ * Discover and invoke each module Lambda once to mitigate cold-start.
48
+ * Best-effort — any failure is logged but never blocks `stage create` success.
49
+ */
50
+ private runAutoWarm;
28
51
  }
@@ -2,6 +2,8 @@ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
4
  import { execSync } from 'node:child_process';
5
+ import { ensureBranches } from '../../lib/ensure-branches.js';
6
+ import { DEFAULT_WARMUP_CONCURRENCY, warmStage } from '../../lib/lambda-warmer.js';
5
7
  import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
6
8
  // Full AWS regions list
7
9
  const AWS_REGIONS = [
@@ -43,10 +45,18 @@ export default class StageCreate extends Command {
43
45
  char: 'a',
44
46
  description: 'AWS Account ID for deployments',
45
47
  }),
48
+ autoCreateBranches: Flags.boolean({
49
+ default: false,
50
+ description: "Ensure the stage branch exists on all linked project remotes before launching the mission (creates missing branches from each remote's default branch)",
51
+ }),
46
52
  autoLaunch: Flags.boolean({
47
53
  default: false,
48
54
  description: 'Automatically launch deployments for this stage',
49
55
  }),
56
+ autoWarm: Flags.boolean({
57
+ default: false,
58
+ description: 'After deployment completes, invoke each module Lambda once to mitigate cold-start. Best-effort — never blocks stage creation. Requires --autoLaunch.',
59
+ }),
50
60
  branchName: Flags.string({
51
61
  char: 'b',
52
62
  description: 'Git branch name',
@@ -85,6 +95,10 @@ export default class StageCreate extends Command {
85
95
  description: 'AWS region(s) for deployment',
86
96
  multiple: true,
87
97
  }),
98
+ warmConcurrency: Flags.integer({
99
+ default: DEFAULT_WARMUP_CONCURRENCY,
100
+ description: `Max in-flight Lambda warmup invocations (default ${DEFAULT_WARMUP_CONCURRENCY}). Only used with --autoWarm.`,
101
+ }),
88
102
  };
89
103
  async run() {
90
104
  const { flags } = await this.parse(StageCreate);
@@ -100,6 +114,9 @@ export default class StageCreate extends Command {
100
114
  this.log(chalk.gray(` Region(s): ${combinedData.region.join(', ')}`));
101
115
  this.log(chalk.gray(` Branch: ${combinedData.branchName}`));
102
116
  this.log(chalk.gray(` Projects: ${combinedData.project.join(', ')}`));
117
+ if (combinedData.autoCreateBranches) {
118
+ await this.runEnsureBranches(combinedData, service);
119
+ }
103
120
  const result = await service.stageCreate({
104
121
  accountId: combinedData.accountId,
105
122
  autoLaunch: combinedData.autoLaunch,
@@ -117,12 +134,30 @@ export default class StageCreate extends Command {
117
134
  const missionId = result.missionId;
118
135
  if (combinedData.autoLaunch && loggingConfig && missionId) {
119
136
  // Start real-time log streaming for mission
120
- await this.streamMissionLogs(loggingConfig, missionId);
137
+ await this.streamMissionLogs(loggingConfig, missionId, {
138
+ autoWarm: Boolean(combinedData.autoWarm),
139
+ regions: combinedData.region,
140
+ stageName: combinedData.name,
141
+ warmConcurrency: combinedData.warmConcurrency,
142
+ });
121
143
  }
122
144
  else {
123
145
  // Standard stage creation (no autoLaunch)
124
146
  this.log(chalk.blue('\n✅ Stage created successfully!'));
147
+ if (result.customDomain) {
148
+ this.log(chalk.cyan(`🌐 Domain: ${result.customDomain}`));
149
+ this.log(` Status: ${result.domainStatus || 'unknown'}`);
150
+ }
125
151
  this.log(JSON.stringify(result, null, 2));
152
+ if (combinedData.autoWarm) {
153
+ // AC1: --auto-warm only meaningful when modules have actually deployed.
154
+ // Without --autoLaunch, the stage exists as a record but no Lambdas
155
+ // have been launched yet — surface that to the user instead of silently
156
+ // skipping.
157
+ this.log(chalk.yellow('\n⚠️ --auto-warm was requested but --autoLaunch was not set. ' +
158
+ 'Lambdas will not be warmed (none have been deployed yet). ' +
159
+ 'Run `hd stage deploy` and re-run with --auto-warm next time, or pair --auto-warm with --autoLaunch.'));
160
+ }
126
161
  }
127
162
  }
128
163
  catch (error) {
@@ -131,6 +166,12 @@ export default class StageCreate extends Command {
131
166
  if (err.message?.includes('EEXIT') && err.oclif?.exit === 0) {
132
167
  return;
133
168
  }
169
+ // Preserve explicit non-zero oclif exits (e.g. from runEnsureBranches)
170
+ // without wrapping them in "Error creating stage" — the inner caller
171
+ // has already printed its own specific error message.
172
+ if (err.oclif?.exit !== undefined && err.oclif.exit !== 0) {
173
+ throw err;
174
+ }
134
175
  const axiosError = error;
135
176
  const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
136
177
  this.log(chalk.red('❌ Error creating stage: ' + errorMessage));
@@ -145,6 +186,57 @@ export default class StageCreate extends Command {
145
186
  const encodedStream = encodeURIComponent(encodeURIComponent(config.logStream));
146
187
  return `https://${config.region}.console.aws.amazon.com/cloudwatch/home?region=${config.region}#logsV2:log-groups/log-group/${encodedGroup}/log-events/${encodedStream}`;
147
188
  }
189
+ /**
190
+ * Ensure the stage branch exists on every linked project remote BEFORE
191
+ * `service.stageCreate` is invoked. Implements the contract described in
192
+ * Story 1.2 ACs 3–5 + 9:
193
+ *
194
+ * - all projects succeed → proceed silently to stageCreate
195
+ * - some projects fail (partial) → log yellow warning + proceed
196
+ * - every project fails → log red error + exit(1) + NEVER
197
+ * call stageCreate
198
+ *
199
+ * The shared `ensureBranches()` lib never throws — all errors are collected
200
+ * into `result.failed`, so we translate the result into the three branches
201
+ * above here.
202
+ */
203
+ async runEnsureBranches(combinedData, service) {
204
+ this.log('');
205
+ this.log(chalk.blue('🌿 Ensuring stage branches on linked project remotes...'));
206
+ const projectSlugs = combinedData.project ?? [];
207
+ const branchName = combinedData.branchName;
208
+ const stageName = combinedData.name;
209
+ if (projectSlugs.length === 0) {
210
+ this.log(chalk.gray('ℹ️ No linked projects — nothing to ensure'));
211
+ return;
212
+ }
213
+ const allModules = await service.moduleList();
214
+ const linkedProjects = allModules
215
+ .filter((m) => m.slug && projectSlugs.includes(m.slug))
216
+ .map((m) => ({
217
+ gitFullRepoPath: m.gitFullRepoPath,
218
+ gitProvider: m.gitProvider,
219
+ slug: m.slug,
220
+ }));
221
+ const verbose = process.env.DEBUG === '1';
222
+ const result = await ensureBranches({
223
+ branchName,
224
+ linkedProjects,
225
+ logger: (msg) => this.log(msg),
226
+ stageName,
227
+ verbose,
228
+ });
229
+ // Hard failure: every project failed. stageCreate MUST NOT run.
230
+ if (result.total > 0 && result.failed === result.total) {
231
+ this.log(chalk.red(`❌ Branch auto-create failed: ${result.failed}/${result.total} remotes failed — aborting stage creation`));
232
+ this.exit(1);
233
+ }
234
+ if (result.failed > 0) {
235
+ this.log(chalk.yellow('⚠️ Branch auto-create completed with warnings — proceeding with stage creation'));
236
+ return;
237
+ }
238
+ this.log(chalk.green(`✅ ${result.created + result.alreadyExisted}/${result.total} branches ensured (${result.created} created, ${result.alreadyExisted} already existed)`));
239
+ }
148
240
  getGitBranch() {
149
241
  try {
150
242
  return execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
@@ -241,7 +333,7 @@ export default class StageCreate extends Command {
241
333
  // Additional options (only prompt if not already set via flags)
242
334
  this.log('');
243
335
  const additionalPrompts = [];
244
- if (flags.production === undefined || flags.production === false) {
336
+ if (flags.production === undefined) {
245
337
  additionalPrompts.push({
246
338
  default: false,
247
339
  message: chalk.yellow('🏭 Is this a production stage?'),
@@ -249,7 +341,7 @@ export default class StageCreate extends Command {
249
341
  type: 'confirm',
250
342
  });
251
343
  }
252
- if (flags.deletionProtection === undefined || flags.deletionProtection === false) {
344
+ if (flags.deletionProtection === undefined) {
253
345
  additionalPrompts.push({
254
346
  default: false,
255
347
  message: chalk.yellow('🔒 Enable deletion protection?'),
@@ -257,7 +349,7 @@ export default class StageCreate extends Command {
257
349
  type: 'confirm',
258
350
  });
259
351
  }
260
- if (flags.defaultStage === undefined || flags.defaultStage === false) {
352
+ if (flags.defaultStage === undefined) {
261
353
  additionalPrompts.push({
262
354
  default: false,
263
355
  message: chalk.yellow('⭐ Set as default stage (fallback)?'),
@@ -265,7 +357,7 @@ export default class StageCreate extends Command {
265
357
  type: 'confirm',
266
358
  });
267
359
  }
268
- if (flags.autoLaunch === undefined || flags.autoLaunch === false) {
360
+ if (flags.autoLaunch === undefined) {
269
361
  additionalPrompts.push({
270
362
  default: false,
271
363
  message: chalk.yellow('🚀 Auto-launch deployments after creation?'),
@@ -284,7 +376,7 @@ export default class StageCreate extends Command {
284
376
  /**
285
377
  * Stream mission logs in real-time
286
378
  */
287
- async streamMissionLogs(loggingConfig, missionId) {
379
+ async streamMissionLogs(loggingConfig, missionId, warmOptions) {
288
380
  this.log(chalk.blue(`\n🚀 Mission started: ${missionId}`));
289
381
  this.log(chalk.gray(` Launching all modules in parallel...`));
290
382
  this.log('');
@@ -300,13 +392,60 @@ export default class StageCreate extends Command {
300
392
  this.log(chalk.cyan(` Mission: ${missionId}`));
301
393
  const consoleUrl = this.buildConsoleUrl(loggingConfig);
302
394
  this.log(chalk.gray(` Logs: ${consoleUrl}`));
395
+ // AC2/AC4/AC5: opt-in post-deploy Lambda warmup. Best-effort.
396
+ if (warmOptions?.autoWarm) {
397
+ await this.runAutoWarm(warmOptions.stageName, warmOptions.regions, warmOptions.warmConcurrency);
398
+ }
303
399
  this.exit(0);
304
400
  }
305
401
  else {
306
402
  this.log(chalk.red(`❌ ${outcome.message}`));
307
403
  const consoleUrl = this.buildConsoleUrl(loggingConfig);
308
404
  this.log(chalk.gray(` Full logs: ${consoleUrl}`));
405
+ // AC5: never warm a failed deploy.
309
406
  this.exit(1);
310
407
  }
311
408
  }
409
+ /**
410
+ * Discover and invoke each module Lambda once to mitigate cold-start.
411
+ * Best-effort — any failure is logged but never blocks `stage create` success.
412
+ */
413
+ async runAutoWarm(stageName, regions, concurrency) {
414
+ this.log('');
415
+ this.log(chalk.blue('🔥 Auto-warm: pre-warming Lambdas to mitigate cold-start...'));
416
+ let summary;
417
+ try {
418
+ summary = await warmStage({
419
+ concurrency,
420
+ logger: (msg) => this.log(chalk.gray(` ${msg}`)),
421
+ regions,
422
+ stageName,
423
+ });
424
+ }
425
+ catch (error) {
426
+ // Defensive: warmStage itself does not throw, but if it ever does we
427
+ // must NOT block stage-create success (AC5).
428
+ this.log(chalk.yellow(`⚠️ Auto-warm failed unexpectedly: ${error.message ?? String(error)} (ignored)`));
429
+ return;
430
+ }
431
+ if (summary.total === 0) {
432
+ this.log(chalk.gray(` No Lambdas matched stage '${stageName}' in ${regions.join(', ')} ` +
433
+ '— nothing to warm.'));
434
+ return;
435
+ }
436
+ const ok = summary.succeeded;
437
+ const ko = summary.failed;
438
+ const sk = summary.skipped;
439
+ if (ko === 0 && sk === 0) {
440
+ this.log(chalk.green(` ✅ Warmed ${ok}/${summary.total} Lambdas`));
441
+ }
442
+ else {
443
+ this.log(chalk.yellow(` ⚠️ Warmed ${ok}/${summary.total} Lambdas (${ko} failed, ${sk} region(s) skipped) — non-fatal`));
444
+ // Surface the first few failures for quick triage. AC5: never throws.
445
+ const failures = summary.results.filter((r) => !r.success).slice(0, 5);
446
+ for (const f of failures) {
447
+ this.log(chalk.gray(` - ${f.functionName} (${f.region}): ${f.error ?? 'unknown error'}`));
448
+ }
449
+ }
450
+ }
312
451
  }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class StageDelete extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }