@hyperdrive.bot/cli 1.0.13 → 1.0.17
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 +4526 -780
- 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 +359 -5
- package/dist/services/hyperdrive-sigv4.js +177 -12
- 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 +41 -13
- 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,97 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
import { printHeader } from '../../utils/table.js';
|
|
6
|
+
export default class ServiceGet extends Command {
|
|
7
|
+
static description = 'Get details of a registered service';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> service get rds-postgres-live',
|
|
10
|
+
'<%= config.bin %> service get rds-postgres-live --json',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
slug: Args.string({
|
|
14
|
+
description: 'Service slug',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static flags = {
|
|
19
|
+
domain: Flags.string({
|
|
20
|
+
char: 'd',
|
|
21
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
22
|
+
}),
|
|
23
|
+
json: Flags.boolean({
|
|
24
|
+
description: 'Output raw JSON response',
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { args, flags } = await this.parse(ServiceGet);
|
|
30
|
+
const isJson = flags.json;
|
|
31
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
32
|
+
const spinner = isJson ? null : ora(`Fetching service "${args.slug}"...`).start();
|
|
33
|
+
try {
|
|
34
|
+
const svc = await service.serviceGet(args.slug);
|
|
35
|
+
spinner?.succeed('Service found');
|
|
36
|
+
if (isJson) {
|
|
37
|
+
this.log(JSON.stringify(svc, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
printHeader(`Service: ${svc.slug}`, (msg) => this.log(msg));
|
|
41
|
+
this.log(` ${chalk.bold('Name:')} ${svc.name}`);
|
|
42
|
+
this.log(` ${chalk.bold('Type:')} ${svc.type}`);
|
|
43
|
+
this.log(` ${chalk.bold('Access:')} ${svc.access === 'private' ? chalk.yellow(svc.access) : chalk.green(svc.access)}`);
|
|
44
|
+
this.log(` ${chalk.bold('Status:')} ${svc.status === 'active' ? chalk.green(svc.status) : chalk.yellow(svc.status)}`);
|
|
45
|
+
this.log(` ${chalk.bold('Endpoint:')} ${svc.endpoint}`);
|
|
46
|
+
if (svc.port)
|
|
47
|
+
this.log(` ${chalk.bold('Port:')} ${svc.port}`);
|
|
48
|
+
if (svc.protocol)
|
|
49
|
+
this.log(` ${chalk.bold('Protocol:')} ${svc.protocol}`);
|
|
50
|
+
this.log(` ${chalk.bold('Account ID:')} ${svc.accountId}`);
|
|
51
|
+
this.log(` ${chalk.bold('Region:')} ${svc.region}`);
|
|
52
|
+
if (svc.networkSlug)
|
|
53
|
+
this.log(` ${chalk.bold('Network:')} ${chalk.cyan(svc.networkSlug)}`);
|
|
54
|
+
// Mask credential ARN — show only last 8 chars
|
|
55
|
+
if (svc.credentialSecretArn) {
|
|
56
|
+
const masked = svc.credentialSecretArn.length > 8
|
|
57
|
+
? '***' + svc.credentialSecretArn.slice(-8)
|
|
58
|
+
: svc.credentialSecretArn;
|
|
59
|
+
this.log(` ${chalk.bold('Credentials:')} ${masked}`);
|
|
60
|
+
}
|
|
61
|
+
if (svc.templateId)
|
|
62
|
+
this.log(` ${chalk.bold('Template:')} ${svc.templateId}`);
|
|
63
|
+
if (svc.stackName)
|
|
64
|
+
this.log(` ${chalk.bold('Stack:')} ${svc.stackName}`);
|
|
65
|
+
this.log(` ${chalk.bold('Created:')} ${new Date(svc.createdAt).toLocaleString()}`);
|
|
66
|
+
this.log(` ${chalk.bold('Updated:')} ${new Date(svc.updatedAt).toLocaleString()}`);
|
|
67
|
+
// Exported env vars
|
|
68
|
+
const envEntries = Object.entries(svc.exportedEnvVars || {});
|
|
69
|
+
if (envEntries.length > 0) {
|
|
70
|
+
this.log(`\n ${chalk.bold('Exported Environment Variables:')}`);
|
|
71
|
+
for (const [key, value] of envEntries) {
|
|
72
|
+
this.log(` ${chalk.bold(key)}: ${value}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Metadata
|
|
76
|
+
const metaEntries = Object.entries(svc.metadata || {});
|
|
77
|
+
if (metaEntries.length > 0) {
|
|
78
|
+
this.log(`\n ${chalk.bold('Metadata:')}`);
|
|
79
|
+
for (const [key, value] of metaEntries) {
|
|
80
|
+
this.log(` ${chalk.bold(key)}: ${value}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
spinner?.fail('Failed');
|
|
86
|
+
const axiosError = error;
|
|
87
|
+
const status = axiosError.response?.status;
|
|
88
|
+
if (status === 404) {
|
|
89
|
+
this.log(chalk.red(`\n❌ Service not found: ${args.slug}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
|
|
93
|
+
}
|
|
94
|
+
this.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ServiceList 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
|
+
network: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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 ServiceList extends Command {
|
|
7
|
+
static description = 'List all registered services';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> service list',
|
|
10
|
+
'<%= config.bin %> service list --type rds-postgres',
|
|
11
|
+
'<%= config.bin %> service list --network prod-us-east-1',
|
|
12
|
+
'<%= config.bin %> service list --json',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
domain: Flags.string({
|
|
16
|
+
char: 'd',
|
|
17
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
18
|
+
}),
|
|
19
|
+
json: Flags.boolean({
|
|
20
|
+
description: 'Output raw JSON response',
|
|
21
|
+
default: false,
|
|
22
|
+
}),
|
|
23
|
+
network: Flags.string({
|
|
24
|
+
description: 'Filter by network slug',
|
|
25
|
+
}),
|
|
26
|
+
type: Flags.string({
|
|
27
|
+
description: 'Filter by service type',
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async run() {
|
|
31
|
+
const { flags } = await this.parse(ServiceList);
|
|
32
|
+
const isJson = flags.json;
|
|
33
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
34
|
+
const spinner = isJson ? null : ora('Fetching services...').start();
|
|
35
|
+
try {
|
|
36
|
+
const services = await service.serviceList({
|
|
37
|
+
type: flags.type,
|
|
38
|
+
network: flags.network,
|
|
39
|
+
});
|
|
40
|
+
spinner?.stop();
|
|
41
|
+
if (isJson) {
|
|
42
|
+
this.log(JSON.stringify(services, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!services || services.length === 0) {
|
|
46
|
+
this.log(chalk.yellow('\nNo services found.'));
|
|
47
|
+
this.log(chalk.gray('Use "hd service register" to add a service.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.log(chalk.green(`\n${services.length} service(s) found:\n`));
|
|
51
|
+
printTable(services, {
|
|
52
|
+
slug: {
|
|
53
|
+
header: 'Slug',
|
|
54
|
+
minWidth: 25,
|
|
55
|
+
get: (row) => chalk.cyan(row.slug),
|
|
56
|
+
},
|
|
57
|
+
name: {
|
|
58
|
+
header: 'Name',
|
|
59
|
+
minWidth: 20,
|
|
60
|
+
},
|
|
61
|
+
type: {
|
|
62
|
+
header: 'Type',
|
|
63
|
+
minWidth: 18,
|
|
64
|
+
},
|
|
65
|
+
access: {
|
|
66
|
+
header: 'Access',
|
|
67
|
+
get: (row) => row.access === 'private' ? chalk.yellow(row.access) : chalk.green(row.access),
|
|
68
|
+
},
|
|
69
|
+
status: {
|
|
70
|
+
header: 'Status',
|
|
71
|
+
get: (row) => row.status === 'active' ? chalk.green(row.status) : chalk.yellow(row.status),
|
|
72
|
+
},
|
|
73
|
+
networkSlug: {
|
|
74
|
+
header: 'Network',
|
|
75
|
+
get: (row) => row.networkSlug || chalk.gray('-'),
|
|
76
|
+
},
|
|
77
|
+
}, (msg) => this.log(msg));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
spinner?.fail('Failed to fetch services');
|
|
81
|
+
const axiosError = error;
|
|
82
|
+
this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
|
|
83
|
+
this.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ServiceRegister extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
access: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'account-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'credential-secret-arn': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
endpoint: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'exported-env-vars': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'network-slug': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
port: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
protocol: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
region: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
slug: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
const SLUG_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
6
|
+
const KNOWN_SERVICE_TYPES = [
|
|
7
|
+
'rds-postgres',
|
|
8
|
+
'rds-mysql',
|
|
9
|
+
'aurora-serverless',
|
|
10
|
+
'elasticache-redis',
|
|
11
|
+
's3-bucket',
|
|
12
|
+
'sqs-queue',
|
|
13
|
+
'custom',
|
|
14
|
+
];
|
|
15
|
+
export default class ServiceRegister extends Command {
|
|
16
|
+
static description = 'Register an external service';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> service register',
|
|
19
|
+
'<%= config.bin %> service register --slug rds-postgres-live --name "Production PostgreSQL" --type rds-postgres --access private --endpoint prod-db.abc123.rds.amazonaws.com --port 5432 --account-id 084309335408 --region us-east-1 --network-slug prod-us-east-1',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
access: Flags.string({ description: 'Access type: private or public', options: ['private', 'public'] }),
|
|
23
|
+
'account-id': Flags.string({ description: 'AWS Account ID where service lives' }),
|
|
24
|
+
'credential-secret-arn': Flags.string({ description: 'Secrets Manager ARN for credentials' }),
|
|
25
|
+
domain: Flags.string({ char: 'd', description: 'Tenant domain (for multi-domain setups)' }),
|
|
26
|
+
endpoint: Flags.string({ description: 'Service endpoint' }),
|
|
27
|
+
'exported-env-vars': Flags.string({ description: 'Exported env vars as JSON string' }),
|
|
28
|
+
name: Flags.string({ description: 'Service display name' }),
|
|
29
|
+
'network-slug': Flags.string({ description: 'Network slug (required for private services)' }),
|
|
30
|
+
port: Flags.integer({ description: 'Service port' }),
|
|
31
|
+
protocol: Flags.string({ description: 'Protocol (e.g. postgres, redis, https)' }),
|
|
32
|
+
region: Flags.string({ char: 'r', description: 'AWS region' }),
|
|
33
|
+
slug: Flags.string({ char: 's', description: 'Service slug' }),
|
|
34
|
+
type: Flags.string({ description: 'Service type' }),
|
|
35
|
+
};
|
|
36
|
+
async run() {
|
|
37
|
+
const { flags } = await this.parse(ServiceRegister);
|
|
38
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
39
|
+
let slug = flags.slug;
|
|
40
|
+
let name = flags.name;
|
|
41
|
+
let type = flags.type;
|
|
42
|
+
let access = flags.access;
|
|
43
|
+
let endpoint = flags.endpoint;
|
|
44
|
+
let port = flags.port;
|
|
45
|
+
let protocol = flags.protocol;
|
|
46
|
+
let credentialSecretArn = flags['credential-secret-arn'];
|
|
47
|
+
let networkSlug = flags['network-slug'];
|
|
48
|
+
let accountId = flags['account-id'];
|
|
49
|
+
let region = flags.region;
|
|
50
|
+
let exportedEnvVars;
|
|
51
|
+
if (flags['exported-env-vars']) {
|
|
52
|
+
try {
|
|
53
|
+
exportedEnvVars = JSON.parse(flags['exported-env-vars']);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
this.log(chalk.red('❌ Invalid JSON for --exported-env-vars'));
|
|
57
|
+
this.exit(1);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Interactive mode if missing required fields
|
|
62
|
+
if (!slug || !name || !type || !access || !endpoint || !accountId || !region) {
|
|
63
|
+
const inquirer = (await import('inquirer')).default;
|
|
64
|
+
if (!slug) {
|
|
65
|
+
const { inputSlug } = await inquirer.prompt([{
|
|
66
|
+
message: chalk.yellow('Service slug:'),
|
|
67
|
+
name: 'inputSlug',
|
|
68
|
+
validate: (input) => SLUG_REGEX.test(input) || 'Must be lowercase alphanumeric with hyphens',
|
|
69
|
+
}]);
|
|
70
|
+
slug = inputSlug;
|
|
71
|
+
}
|
|
72
|
+
if (!name) {
|
|
73
|
+
const { inputName } = await inquirer.prompt([{
|
|
74
|
+
message: chalk.yellow('Service display name:'),
|
|
75
|
+
name: 'inputName',
|
|
76
|
+
validate: (input) => input.length > 0 || 'Name is required',
|
|
77
|
+
}]);
|
|
78
|
+
name = inputName;
|
|
79
|
+
}
|
|
80
|
+
if (!type) {
|
|
81
|
+
const { inputType } = await inquirer.prompt([{
|
|
82
|
+
choices: KNOWN_SERVICE_TYPES,
|
|
83
|
+
message: chalk.yellow('Service type:'),
|
|
84
|
+
name: 'inputType',
|
|
85
|
+
type: 'list',
|
|
86
|
+
}]);
|
|
87
|
+
type = inputType;
|
|
88
|
+
}
|
|
89
|
+
if (!access) {
|
|
90
|
+
const { inputAccess } = await inquirer.prompt([{
|
|
91
|
+
choices: [
|
|
92
|
+
{ name: 'Private (requires VPC/network)', value: 'private' },
|
|
93
|
+
{ name: 'Public (no VPC constraint)', value: 'public' },
|
|
94
|
+
],
|
|
95
|
+
message: chalk.yellow('Access type:'),
|
|
96
|
+
name: 'inputAccess',
|
|
97
|
+
type: 'list',
|
|
98
|
+
}]);
|
|
99
|
+
access = inputAccess;
|
|
100
|
+
}
|
|
101
|
+
if (!endpoint) {
|
|
102
|
+
const { inputEndpoint } = await inquirer.prompt([{
|
|
103
|
+
message: chalk.yellow('Service endpoint (hostname or URL):'),
|
|
104
|
+
name: 'inputEndpoint',
|
|
105
|
+
validate: (input) => input.length > 0 || 'Endpoint is required',
|
|
106
|
+
}]);
|
|
107
|
+
endpoint = inputEndpoint;
|
|
108
|
+
}
|
|
109
|
+
if (port === undefined) {
|
|
110
|
+
const { inputPort } = await inquirer.prompt([{
|
|
111
|
+
message: chalk.yellow('Port (or leave empty):'),
|
|
112
|
+
name: 'inputPort',
|
|
113
|
+
}]);
|
|
114
|
+
if (inputPort)
|
|
115
|
+
port = parseInt(inputPort, 10);
|
|
116
|
+
}
|
|
117
|
+
if (!protocol) {
|
|
118
|
+
const { inputProtocol } = await inquirer.prompt([{
|
|
119
|
+
message: chalk.yellow('Protocol (e.g. postgres, redis, https — or leave empty):'),
|
|
120
|
+
name: 'inputProtocol',
|
|
121
|
+
}]);
|
|
122
|
+
if (inputProtocol)
|
|
123
|
+
protocol = inputProtocol;
|
|
124
|
+
}
|
|
125
|
+
if (!credentialSecretArn) {
|
|
126
|
+
const { inputCredArn } = await inquirer.prompt([{
|
|
127
|
+
message: chalk.yellow('Credential Secret ARN (or leave empty):'),
|
|
128
|
+
name: 'inputCredArn',
|
|
129
|
+
}]);
|
|
130
|
+
if (inputCredArn)
|
|
131
|
+
credentialSecretArn = inputCredArn;
|
|
132
|
+
}
|
|
133
|
+
if (access === 'private' && !networkSlug) {
|
|
134
|
+
const { inputNetworkSlug } = await inquirer.prompt([{
|
|
135
|
+
message: chalk.yellow('Network slug (required for private services):'),
|
|
136
|
+
name: 'inputNetworkSlug',
|
|
137
|
+
validate: (input) => input.length > 0 || 'Network slug is required for private services',
|
|
138
|
+
}]);
|
|
139
|
+
networkSlug = inputNetworkSlug;
|
|
140
|
+
}
|
|
141
|
+
if (!accountId) {
|
|
142
|
+
const { inputAccountId } = await inquirer.prompt([{
|
|
143
|
+
message: chalk.yellow('AWS Account ID:'),
|
|
144
|
+
name: 'inputAccountId',
|
|
145
|
+
validate: (input) => /^\d{12}$/.test(input) || 'Must be exactly 12 digits',
|
|
146
|
+
}]);
|
|
147
|
+
accountId = inputAccountId;
|
|
148
|
+
}
|
|
149
|
+
if (!region) {
|
|
150
|
+
const { inputRegion } = await inquirer.prompt([{
|
|
151
|
+
choices: [
|
|
152
|
+
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
|
153
|
+
'sa-east-1', 'eu-west-1', 'eu-west-2', 'eu-central-1',
|
|
154
|
+
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
|
|
155
|
+
],
|
|
156
|
+
default: 'us-east-1',
|
|
157
|
+
message: chalk.yellow('AWS Region:'),
|
|
158
|
+
name: 'inputRegion',
|
|
159
|
+
type: 'list',
|
|
160
|
+
}]);
|
|
161
|
+
region = inputRegion;
|
|
162
|
+
}
|
|
163
|
+
if (!exportedEnvVars) {
|
|
164
|
+
const { inputEnvVars } = await inquirer.prompt([{
|
|
165
|
+
message: chalk.yellow('Exported env vars JSON (e.g. {"DATABASE_URL":"postgres://..."}, or leave empty):'),
|
|
166
|
+
name: 'inputEnvVars',
|
|
167
|
+
}]);
|
|
168
|
+
if (inputEnvVars) {
|
|
169
|
+
try {
|
|
170
|
+
exportedEnvVars = JSON.parse(inputEnvVars);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
this.log(chalk.yellow('Invalid JSON — skipping exported env vars.'));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Validate: private services require networkSlug
|
|
179
|
+
if (access === 'private' && !networkSlug) {
|
|
180
|
+
this.log(chalk.red('❌ Private services require --network-slug'));
|
|
181
|
+
this.exit(1);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Validate slug
|
|
185
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
186
|
+
this.log(chalk.red(`❌ Invalid slug format: "${slug}"`));
|
|
187
|
+
this.exit(1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const spinner = ora(`Registering service "${slug}"...`).start();
|
|
191
|
+
try {
|
|
192
|
+
await service.serviceRegister({
|
|
193
|
+
slug: slug,
|
|
194
|
+
name: name,
|
|
195
|
+
type: type,
|
|
196
|
+
access: access,
|
|
197
|
+
endpoint: endpoint,
|
|
198
|
+
port,
|
|
199
|
+
protocol,
|
|
200
|
+
credentialSecretArn,
|
|
201
|
+
networkSlug,
|
|
202
|
+
accountId: accountId,
|
|
203
|
+
region: region,
|
|
204
|
+
exportedEnvVars,
|
|
205
|
+
});
|
|
206
|
+
spinner.succeed(chalk.green(`Service registered: ${slug}`));
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
spinner.fail('Registration failed');
|
|
210
|
+
const axiosError = error;
|
|
211
|
+
this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
|
|
212
|
+
this.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ServiceRestore extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
from: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
target: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
network: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'instance-class': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
import { pollLifecycle } from '../../utils/lifecycle-poller.js';
|
|
6
|
+
export default class ServiceRestore extends Command {
|
|
7
|
+
static description = 'Restore a service from a previously created backup';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> service restore rds-postgres-live --from 018d4e7f-af32-7000-8000-000000000001',
|
|
10
|
+
'<%= config.bin %> service restore rds-postgres-live --from 018d4e7f --target rds-postgres-staging',
|
|
11
|
+
'<%= config.bin %> service restore rds-postgres-live --from 018d4e7f --target rds-new --network prod-us-east-1 --instance-class db.t3.medium',
|
|
12
|
+
'<%= config.bin %> service restore rds-postgres-live --from 018d4e7f --no-wait --json',
|
|
13
|
+
'<%= config.bin %> service restore rds-postgres-live --from 018d4e7f --force',
|
|
14
|
+
];
|
|
15
|
+
static args = {
|
|
16
|
+
slug: Args.string({
|
|
17
|
+
description: 'Service slug to restore',
|
|
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
|
+
from: Flags.string({
|
|
27
|
+
description: 'Backup ID to restore from',
|
|
28
|
+
required: true,
|
|
29
|
+
}),
|
|
30
|
+
target: Flags.string({
|
|
31
|
+
description: 'Target service slug for a new instance (omit for in-place restore)',
|
|
32
|
+
}),
|
|
33
|
+
network: Flags.string({
|
|
34
|
+
description: 'Network slug for the restored instance (for new-target restores)',
|
|
35
|
+
}),
|
|
36
|
+
'instance-class': Flags.string({
|
|
37
|
+
description: 'RDS instance class override (e.g., db.t3.medium)',
|
|
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 restore to complete',
|
|
45
|
+
default: false,
|
|
46
|
+
}),
|
|
47
|
+
force: Flags.boolean({
|
|
48
|
+
description: 'Skip confirmation prompt',
|
|
49
|
+
default: false,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
async run() {
|
|
53
|
+
const { args, flags } = await this.parse(ServiceRestore);
|
|
54
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
55
|
+
const isJson = flags.json;
|
|
56
|
+
// Confirmation prompt (unless --force or --json)
|
|
57
|
+
if (!flags.force && !isJson) {
|
|
58
|
+
const action = flags.target
|
|
59
|
+
? `create new service "${flags.target}" from`
|
|
60
|
+
: `replace existing service "${args.slug}" from`;
|
|
61
|
+
this.log(chalk.yellow(`\nRestore service: ${action} backup ${flags.from}`));
|
|
62
|
+
const response = await new Promise((resolve) => {
|
|
63
|
+
process.stdout.write(chalk.yellow('Are you sure you want to proceed? (y/N): '));
|
|
64
|
+
process.stdin.setEncoding('utf-8');
|
|
65
|
+
process.stdin.once('data', (data) => {
|
|
66
|
+
resolve(data.toString().trim().toLowerCase());
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
if (response !== 'y' && response !== 'yes') {
|
|
70
|
+
this.log(chalk.gray('Aborted.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const spinner = isJson ? null : ora('Submitting restore request...').start();
|
|
75
|
+
try {
|
|
76
|
+
const backup = await service.serviceRestore(args.slug, {
|
|
77
|
+
backupId: flags.from,
|
|
78
|
+
targetSlug: flags.target,
|
|
79
|
+
networkSlug: flags.network,
|
|
80
|
+
instanceClass: flags['instance-class'],
|
|
81
|
+
});
|
|
82
|
+
if (isJson && flags['no-wait']) {
|
|
83
|
+
this.log(JSON.stringify(backup, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
spinner?.succeed(chalk.green(`Restore initiated: ${backup.id}`));
|
|
87
|
+
if (!isJson) {
|
|
88
|
+
this.log(` Service: ${chalk.cyan(args.slug)}`);
|
|
89
|
+
this.log(` Backup: ${chalk.cyan(flags.from)}`);
|
|
90
|
+
this.log(` Restore ID: ${chalk.cyan(backup.id)}`);
|
|
91
|
+
if (flags.target)
|
|
92
|
+
this.log(` Target: ${chalk.cyan(flags.target)}`);
|
|
93
|
+
this.log(` Status: ${chalk.yellow(backup.status)}`);
|
|
94
|
+
this.log('');
|
|
95
|
+
}
|
|
96
|
+
if (flags['no-wait']) {
|
|
97
|
+
if (!isJson) {
|
|
98
|
+
this.log(chalk.gray(`Check status: hd service get ${args.slug}`));
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Poll for restore completion
|
|
103
|
+
const result = await pollLifecycle({
|
|
104
|
+
pollFn: () => service.serviceGetBackup(args.slug, backup.id),
|
|
105
|
+
getStatus: (b) => b.status,
|
|
106
|
+
terminalStates: new Set(['completed', 'failed']),
|
|
107
|
+
successStates: new Set(['completed']),
|
|
108
|
+
getErrorMessage: (b) => b.errorMessage,
|
|
109
|
+
operationLabel: 'Restoring',
|
|
110
|
+
}, isJson);
|
|
111
|
+
if (isJson) {
|
|
112
|
+
this.log(JSON.stringify(result.entity, null, 2));
|
|
113
|
+
}
|
|
114
|
+
if (result.timedOut) {
|
|
115
|
+
if (!isJson) {
|
|
116
|
+
this.log(chalk.yellow('The restore job may still be running.'));
|
|
117
|
+
this.log(chalk.gray(`Check status: hd service get ${args.slug}`));
|
|
118
|
+
}
|
|
119
|
+
this.exit(1);
|
|
120
|
+
}
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
this.exit(1);
|
|
123
|
+
}
|
|
124
|
+
// Success summary
|
|
125
|
+
if (!isJson) {
|
|
126
|
+
this.log('');
|
|
127
|
+
if (flags.target) {
|
|
128
|
+
this.log(chalk.yellow('Next step:'));
|
|
129
|
+
this.log(chalk.gray(` hd service get ${flags.target}`));
|
|
130
|
+
this.log(chalk.gray(` hd service bind ${flags.target} <module> --stage <stage>`));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.log(chalk.yellow('Service endpoint updated. Check:'));
|
|
134
|
+
this.log(chalk.gray(` hd service get ${args.slug}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
spinner?.fail('Restore request failed');
|
|
140
|
+
const axiosError = error;
|
|
141
|
+
const status = axiosError.response?.status;
|
|
142
|
+
const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
|
|
143
|
+
if (status === 400) {
|
|
144
|
+
this.log(chalk.red(`Validation error: ${errorMessage}`));
|
|
145
|
+
}
|
|
146
|
+
else if (status === 404) {
|
|
147
|
+
this.log(chalk.red(`Not found: ${errorMessage}`));
|
|
148
|
+
}
|
|
149
|
+
else if (status === 409) {
|
|
150
|
+
this.log(chalk.red(`Conflict: ${errorMessage}`));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
this.log(chalk.red(`Error: ${errorMessage}`));
|
|
154
|
+
}
|
|
155
|
+
this.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ServiceSeed extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
from: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
inline: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'allow-production': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|