@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.
- package/README.md +1495 -474
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.js +239 -0
- package/dist/commands/deployment/create.js +10 -2
- package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
- package/dist/commands/domain/set-production.js +27 -0
- package/dist/commands/git/list-open-prs.d.ts +12 -0
- package/dist/commands/git/list-open-prs.js +87 -0
- package/dist/commands/hook/add.d.ts +22 -0
- package/dist/commands/hook/add.js +299 -0
- package/dist/commands/hook/list.d.ts +11 -0
- package/dist/commands/hook/list.js +111 -0
- package/dist/commands/hook/logs.d.ts +13 -0
- package/dist/commands/hook/logs.js +124 -0
- package/dist/commands/hook/remove.d.ts +12 -0
- package/dist/commands/hook/remove.js +115 -0
- package/dist/commands/hook/toggle.d.ts +12 -0
- package/dist/commands/hook/toggle.js +125 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +49 -9
- package/dist/commands/module/bindings.d.ts +14 -0
- package/dist/commands/module/bindings.js +125 -0
- package/dist/commands/module/create.d.ts +3 -0
- package/dist/commands/module/create.js +156 -78
- package/dist/commands/module/list.d.ts +1 -0
- package/dist/commands/module/list.js +22 -1
- package/dist/commands/module/sync.d.ts +29 -0
- package/dist/commands/module/sync.js +409 -0
- package/dist/commands/module/unlink.d.ts +11 -0
- package/dist/commands/module/unlink.js +77 -0
- package/dist/commands/module/update.d.ts +10 -0
- package/dist/commands/module/update.js +168 -5
- package/dist/commands/network/discover.d.ts +12 -0
- package/dist/commands/network/discover.js +210 -0
- package/dist/commands/network/get.d.ts +13 -0
- package/dist/commands/network/get.js +90 -0
- package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
- package/dist/commands/network/list.js +71 -0
- package/dist/commands/network/register.d.ts +16 -0
- package/dist/commands/network/register.js +144 -0
- package/dist/commands/parameter/sync.d.ts +13 -0
- package/dist/commands/parameter/sync.js +69 -1
- package/dist/commands/project/sync.d.ts +5 -11
- package/dist/commands/project/sync.js +12 -381
- package/dist/commands/seed.d.ts +93 -0
- package/dist/commands/seed.js +324 -0
- package/dist/commands/service/backup.d.ts +17 -0
- package/dist/commands/service/backup.js +156 -0
- package/dist/commands/service/backups.d.ts +14 -0
- package/dist/commands/service/backups.js +110 -0
- package/dist/commands/service/bind.d.ts +16 -0
- package/dist/commands/service/bind.js +106 -0
- package/dist/commands/service/bindings.d.ts +13 -0
- package/dist/commands/service/bindings.js +78 -0
- package/dist/commands/service/clone.d.ts +19 -0
- package/dist/commands/service/clone.js +153 -0
- package/dist/commands/service/create.d.ts +16 -0
- package/dist/commands/service/create.js +212 -0
- package/dist/commands/service/get.d.ts +13 -0
- package/dist/commands/service/get.js +97 -0
- package/dist/commands/service/list.d.ts +12 -0
- package/dist/commands/service/list.js +86 -0
- package/dist/commands/service/register.d.ts +21 -0
- package/dist/commands/service/register.js +215 -0
- package/dist/commands/service/restore.d.ts +19 -0
- package/dist/commands/service/restore.js +158 -0
- package/dist/commands/service/seed.d.ts +17 -0
- package/dist/commands/service/seed.js +173 -0
- package/dist/commands/service/templates.d.ts +10 -0
- package/dist/commands/service/templates.js +66 -0
- package/dist/commands/service/unbind.d.ts +15 -0
- package/dist/commands/service/unbind.js +74 -0
- package/dist/commands/stage/create.d.ts +23 -0
- package/dist/commands/stage/create.js +145 -6
- package/dist/commands/stage/delete.d.ts +11 -0
- package/dist/commands/stage/delete.js +85 -0
- package/dist/commands/stage/deploy.d.ts +34 -0
- package/dist/commands/stage/deploy.js +294 -0
- package/dist/commands/stage/ensure-branches.d.ts +23 -0
- package/dist/commands/stage/ensure-branches.js +101 -0
- package/dist/commands/stage/list.js +4 -0
- package/dist/commands/stage/status.d.ts +14 -0
- package/dist/commands/stage/status.js +100 -0
- package/dist/commands/{jira → tracker}/connect.js +32 -23
- package/dist/commands/tracker/hook/add.d.ts +25 -0
- package/dist/commands/tracker/hook/add.js +284 -0
- package/dist/commands/{jira → tracker}/hook/list.js +20 -11
- package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
- package/dist/commands/tracker/hook/logs.js +126 -0
- package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
- package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
- package/dist/commands/tracker/project/init.d.ts +17 -0
- package/dist/commands/tracker/project/init.js +178 -0
- package/dist/commands/tracker/project/link-module.d.ts +17 -0
- package/dist/commands/tracker/project/link-module.js +287 -0
- package/dist/commands/tracker/project/list-modules.d.ts +11 -0
- package/dist/commands/tracker/project/list-modules.js +117 -0
- package/dist/commands/tracker/project/list.d.ts +10 -0
- package/dist/commands/tracker/project/list.js +90 -0
- package/dist/commands/tracker/project/status.d.ts +13 -0
- package/dist/commands/tracker/project/status.js +168 -0
- package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
- package/dist/commands/tracker/project/unlink-module.js +251 -0
- package/dist/commands/{jira → tracker}/status.js +3 -3
- package/dist/lib/ensure-branches.d.ts +53 -0
- package/dist/lib/ensure-branches.js +149 -0
- package/dist/lib/git-providers/github.d.ts +16 -0
- package/dist/lib/git-providers/github.js +157 -0
- package/dist/lib/git-providers/gitlab.d.ts +16 -0
- package/dist/lib/git-providers/gitlab.js +148 -0
- package/dist/lib/git-providers/index.d.ts +67 -0
- package/dist/lib/git-providers/index.js +39 -0
- package/dist/lib/lambda-warmer.d.ts +106 -0
- package/dist/lib/lambda-warmer.js +189 -0
- package/dist/services/hyperdrive-sigv4.d.ts +360 -5
- package/dist/services/hyperdrive-sigv4.js +192 -24
- package/dist/utils/hook-flow.d.ts +60 -3
- package/dist/utils/hook-flow.js +437 -2
- package/dist/utils/hook-normalize.d.ts +6 -0
- package/dist/utils/hook-normalize.js +33 -0
- package/dist/utils/lifecycle-poller.d.ts +32 -0
- package/dist/utils/lifecycle-poller.js +72 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.js +88 -0
- package/dist/utils/summary-display.js +1 -1
- package/dist/utils/tracker-project-flow.d.ts +84 -0
- package/dist/utils/tracker-project-flow.js +564 -0
- package/package.json +35 -7
- package/dist/commands/auth/login.d.ts +0 -16
- package/dist/commands/auth/login.js +0 -179
- package/dist/commands/auth/logout.js +0 -116
- package/dist/commands/auth/refresh.d.ts +0 -6
- package/dist/commands/auth/refresh.js +0 -66
- package/dist/commands/auth/status.d.ts +0 -6
- package/dist/commands/auth/status.js +0 -63
- package/dist/commands/config/get.d.ts +0 -9
- package/dist/commands/config/get.js +0 -37
- package/dist/commands/config/set.d.ts +0 -10
- package/dist/commands/config/set.js +0 -48
- package/dist/commands/config/show.d.ts +0 -6
- package/dist/commands/config/show.js +0 -10
- package/dist/commands/domain/current.d.ts +0 -6
- package/dist/commands/domain/current.js +0 -18
- package/dist/commands/domain/list.d.ts +0 -6
- package/dist/commands/domain/list.js +0 -42
- package/dist/commands/domain/switch.js +0 -40
- package/dist/commands/jira/hook/add.js +0 -147
- package/dist/services/tenant-service.d.ts +0 -127
- package/dist/services/tenant-service.js +0 -396
- package/dist/utils/auth-flow.d.ts +0 -147
- package/dist/utils/auth-flow.js +0 -479
- package/oclif.manifest.json +0 -3519
- /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|