@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,106 @@
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 ServiceBind extends Command {
6
+ static description = 'Bind a service to a module+stage';
7
+ static examples = [
8
+ '<%= config.bin %> service bind rds-postgres-live my-api --stage live',
9
+ '<%= config.bin %> service bind rds-postgres-live my-api --stage live --template-vars \'{"dbName":"mydb"}\'',
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
+ 'env-var-mapping': Flags.string({
27
+ description: 'Environment variable mapping overrides as JSON',
28
+ }),
29
+ stage: Flags.string({
30
+ description: 'Deployment stage',
31
+ required: true,
32
+ }),
33
+ 'template-vars': Flags.string({
34
+ description: 'Template variables as JSON',
35
+ }),
36
+ };
37
+ async run() {
38
+ const { args, flags } = await this.parse(ServiceBind);
39
+ const service = new HyperdriveSigV4Service(flags.domain);
40
+ let envVarMapping;
41
+ let templateVars;
42
+ if (flags['env-var-mapping']) {
43
+ try {
44
+ envVarMapping = JSON.parse(flags['env-var-mapping']);
45
+ }
46
+ catch {
47
+ this.log(chalk.red('❌ Invalid JSON for --env-var-mapping'));
48
+ this.exit(1);
49
+ return;
50
+ }
51
+ }
52
+ if (flags['template-vars']) {
53
+ try {
54
+ templateVars = JSON.parse(flags['template-vars']);
55
+ }
56
+ catch {
57
+ this.log(chalk.red('❌ Invalid JSON for --template-vars'));
58
+ this.exit(1);
59
+ return;
60
+ }
61
+ }
62
+ const spinner = ora(`Binding "${args.service}" to "${args.module}" (stage: ${flags.stage})...`).start();
63
+ try {
64
+ const binding = await service.serviceBind(args.service, {
65
+ moduleSlug: args.module,
66
+ stage: flags.stage,
67
+ envVarMapping,
68
+ templateVars,
69
+ });
70
+ spinner.succeed(chalk.green(`Service bound: ${args.service} → ${args.module} (${flags.stage})`));
71
+ // Show VPC anchor info if the service details are returned
72
+ try {
73
+ const svc = await service.serviceGet(args.service);
74
+ if (svc.access === 'private' && svc.networkSlug) {
75
+ this.log(chalk.blue(`\n Module bound via network: ${chalk.cyan(svc.networkSlug)}`));
76
+ this.log(chalk.gray(' All other private services for this module+stage must share this network.'));
77
+ }
78
+ }
79
+ catch {
80
+ // Non-critical — skip VPC info display
81
+ }
82
+ }
83
+ catch (error) {
84
+ spinner.fail('Binding failed');
85
+ const axiosError = error;
86
+ const status = axiosError.response?.status;
87
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
88
+ if (status === 409) {
89
+ // VPC conflict — display actionable error
90
+ this.log(chalk.red(`\n❌ ${errorMessage}`));
91
+ this.log('');
92
+ this.log(chalk.yellow(' Options:'));
93
+ this.log(chalk.yellow(' 1. Register the service in the correct network'));
94
+ this.log(chalk.yellow(' 2. Unbind existing private services first'));
95
+ this.log(chalk.yellow(' 3. Set up VPC peering between the two networks'));
96
+ }
97
+ else if (status === 404) {
98
+ this.log(chalk.red(`\n❌ Not found: ${errorMessage}`));
99
+ }
100
+ else {
101
+ this.log(chalk.red(`\n❌ ${errorMessage}`));
102
+ }
103
+ this.exit(1);
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceBindings 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
+ };
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,78 @@
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 { printTable } from '../../utils/table.js';
6
+ export default class ServiceBindings extends Command {
7
+ static description = 'List all bindings for a service';
8
+ static examples = [
9
+ '<%= config.bin %> service bindings rds-postgres-live',
10
+ '<%= config.bin %> service bindings rds-postgres-live --json',
11
+ ];
12
+ static args = {
13
+ service: 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(ServiceBindings);
30
+ const isJson = flags.json;
31
+ const service = new HyperdriveSigV4Service(flags.domain);
32
+ const spinner = isJson ? null : ora(`Fetching bindings for "${args.service}"...`).start();
33
+ try {
34
+ const bindings = await service.serviceBindings(args.service);
35
+ spinner?.stop();
36
+ if (isJson) {
37
+ this.log(JSON.stringify(bindings, null, 2));
38
+ return;
39
+ }
40
+ if (!bindings || bindings.length === 0) {
41
+ this.log(chalk.yellow(`\nNo bindings found for service "${args.service}".`));
42
+ return;
43
+ }
44
+ this.log(chalk.green(`\n${bindings.length} binding(s) for "${args.service}":\n`));
45
+ printTable(bindings, {
46
+ stage: {
47
+ header: 'Stage',
48
+ minWidth: 15,
49
+ get: (row) => chalk.cyan(row.stage),
50
+ },
51
+ moduleSlug: {
52
+ header: 'Module',
53
+ minWidth: 20,
54
+ },
55
+ envVars: {
56
+ header: 'Env Vars',
57
+ get: (row) => String(Object.keys(row.envVarMapping || {}).length),
58
+ },
59
+ createdAt: {
60
+ header: 'Created',
61
+ get: (row) => new Date(row.createdAt).toLocaleDateString(),
62
+ },
63
+ }, (msg) => this.log(msg));
64
+ }
65
+ catch (error) {
66
+ spinner?.fail('Failed');
67
+ const axiosError = error;
68
+ const status = axiosError.response?.status;
69
+ if (status === 404) {
70
+ this.log(chalk.red(`\n❌ Service not found: ${args.service}`));
71
+ }
72
+ else {
73
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
74
+ }
75
+ this.exit(1);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,19 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceClone extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ source: 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
+ name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ network: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ account: 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,153 @@
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 ServiceClone extends Command {
7
+ static description = 'Clone a service (snapshot source, restore as new instance in target network)';
8
+ static examples = [
9
+ '<%= config.bin %> service clone rds-postgres-live --name rds-postgres-staging --network staging-us-east-1',
10
+ '<%= config.bin %> service clone rds-postgres-live --name rds-postgres-dr --network dr-us-west-2 --account 123456789012',
11
+ '<%= config.bin %> service clone rds-postgres-live --name rds-staging --network staging-vpc --instance-class db.t3.medium',
12
+ '<%= config.bin %> service clone rds-postgres-live --name rds-staging --network staging-vpc --no-wait --json',
13
+ '<%= config.bin %> service clone rds-postgres-live --name rds-staging --network staging-vpc --force',
14
+ ];
15
+ static args = {
16
+ source: Args.string({
17
+ description: 'Source service slug to clone',
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
+ name: Flags.string({
27
+ description: 'Target service slug for the cloned instance',
28
+ required: true,
29
+ }),
30
+ network: Flags.string({
31
+ description: 'Target network slug to place the cloned instance in',
32
+ required: true,
33
+ }),
34
+ account: Flags.string({
35
+ description: 'Target AWS account ID (for cross-account clones)',
36
+ }),
37
+ 'instance-class': Flags.string({
38
+ description: 'RDS instance class override (e.g., db.t3.medium)',
39
+ }),
40
+ json: Flags.boolean({
41
+ description: 'Output raw JSON response',
42
+ default: false,
43
+ }),
44
+ 'no-wait': Flags.boolean({
45
+ description: 'Do not wait for clone to complete',
46
+ default: false,
47
+ }),
48
+ force: Flags.boolean({
49
+ description: 'Skip confirmation prompt',
50
+ default: false,
51
+ }),
52
+ };
53
+ async run() {
54
+ const { args, flags } = await this.parse(ServiceClone);
55
+ const service = new HyperdriveSigV4Service(flags.domain);
56
+ const isJson = flags.json;
57
+ // Confirmation prompt (unless --force or --json)
58
+ if (!flags.force && !isJson) {
59
+ this.log(chalk.yellow(`\nClone service "${args.source}" to "${flags.name}" in network "${flags.network}"`));
60
+ this.log(chalk.gray('This creates a new service with a fresh database copy.\n'));
61
+ const response = await new Promise((resolve) => {
62
+ process.stdout.write(chalk.yellow('Are you sure you want to proceed? (y/N): '));
63
+ process.stdin.setEncoding('utf-8');
64
+ process.stdin.once('data', (data) => {
65
+ resolve(data.toString().trim().toLowerCase());
66
+ });
67
+ });
68
+ if (response !== 'y' && response !== 'yes') {
69
+ this.log(chalk.gray('Aborted.'));
70
+ return;
71
+ }
72
+ }
73
+ const spinner = isJson ? null : ora('Submitting clone request...').start();
74
+ try {
75
+ const backup = await service.serviceClone(args.source, {
76
+ targetSlug: flags.name,
77
+ targetNetworkSlug: flags.network,
78
+ targetAccountId: flags.account,
79
+ instanceClass: flags['instance-class'],
80
+ });
81
+ if (isJson && flags['no-wait']) {
82
+ this.log(JSON.stringify(backup, null, 2));
83
+ return;
84
+ }
85
+ spinner?.succeed(chalk.green(`Clone initiated: ${backup.id}`));
86
+ if (!isJson) {
87
+ this.log(` Source: ${chalk.cyan(args.source)}`);
88
+ this.log(` Target: ${chalk.cyan(flags.name)}`);
89
+ this.log(` Network: ${chalk.cyan(flags.network)}`);
90
+ if (flags.account)
91
+ this.log(` Account: ${chalk.cyan(flags.account)}`);
92
+ this.log(` Clone ID: ${chalk.cyan(backup.id)}`);
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.source}`));
99
+ }
100
+ return;
101
+ }
102
+ // Poll for clone completion with phase-aware display
103
+ const result = await pollLifecycle({
104
+ pollFn: () => service.serviceGetBackup(args.source, backup.id),
105
+ getStatus: (b) => b.status,
106
+ terminalStates: new Set(['completed', 'failed']),
107
+ successStates: new Set(['completed']),
108
+ getPhase: (b) => b.metadata?.phase,
109
+ getErrorMessage: (b) => b.errorMessage,
110
+ operationLabel: 'Cloning',
111
+ }, isJson);
112
+ if (isJson) {
113
+ this.log(JSON.stringify(result.entity, null, 2));
114
+ }
115
+ if (result.timedOut) {
116
+ if (!isJson) {
117
+ this.log(chalk.yellow('The clone job may still be running.'));
118
+ this.log(chalk.gray(`Check status: hd service get ${args.source}`));
119
+ }
120
+ this.exit(1);
121
+ }
122
+ if (!result.success) {
123
+ this.exit(1);
124
+ }
125
+ // Success summary
126
+ if (!isJson) {
127
+ this.log('');
128
+ this.log(chalk.yellow('Next steps:'));
129
+ this.log(chalk.gray(` hd service get ${flags.name}`));
130
+ this.log(chalk.gray(` hd service bind ${flags.name} <module> --stage <stage>`));
131
+ }
132
+ }
133
+ catch (error) {
134
+ spinner?.fail('Clone request failed');
135
+ const axiosError = error;
136
+ const status = axiosError.response?.status;
137
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
138
+ if (status === 400) {
139
+ this.log(chalk.red(`Validation error: ${errorMessage}`));
140
+ }
141
+ else if (status === 404) {
142
+ this.log(chalk.red(`Not found: ${errorMessage}`));
143
+ }
144
+ else if (status === 409) {
145
+ this.log(chalk.red(`Conflict: ${errorMessage}`));
146
+ }
147
+ else {
148
+ this.log(chalk.red(`Error: ${errorMessage}`));
149
+ }
150
+ this.exit(1);
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceCreate 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
+ template: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ network: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ slug: 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
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'no-interactive': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,212 @@
1
+ import { Command, Flags, ux } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ const POLL_INTERVAL_MS = 10_000;
6
+ const PROVISION_TIMEOUT_MS = 30 * 60 * 1000;
7
+ export default class ServiceCreate extends Command {
8
+ static description = 'Provision a new service from the template catalog';
9
+ static examples = [
10
+ '<%= config.bin %> service create --template rds-postgres-v1 --network prod-us-east-1 --stage live',
11
+ '<%= config.bin %> service create --template s3-bucket-v1 --stage dev --slug my-bucket --no-interactive',
12
+ '<%= config.bin %> service create --template rds-postgres-v1 --stage dev -p dbName=myapp -p instanceClass=db.t3.small --no-interactive',
13
+ ];
14
+ static flags = {
15
+ domain: Flags.string({
16
+ char: 'd',
17
+ description: 'Tenant domain (for multi-domain setups)',
18
+ }),
19
+ template: Flags.string({
20
+ char: 't',
21
+ description: 'Template ID (e.g., rds-postgres-v1)',
22
+ required: true,
23
+ }),
24
+ network: Flags.string({
25
+ char: 'n',
26
+ description: 'Network slug (required for private services)',
27
+ }),
28
+ name: Flags.string({
29
+ description: 'Service display name',
30
+ }),
31
+ slug: Flags.string({
32
+ description: 'Service slug (auto-generated if not provided)',
33
+ }),
34
+ stage: Flags.string({
35
+ char: 's',
36
+ description: 'Deployment stage',
37
+ required: true,
38
+ }),
39
+ parameter: Flags.string({
40
+ char: 'p',
41
+ description: 'Template parameter as key=value (repeatable, for CI/CD)',
42
+ multiple: true,
43
+ }),
44
+ 'no-interactive': Flags.boolean({
45
+ description: 'Skip interactive prompts, use defaults',
46
+ default: false,
47
+ }),
48
+ };
49
+ async run() {
50
+ const { flags } = await this.parse(ServiceCreate);
51
+ const service = new HyperdriveSigV4Service(flags.domain);
52
+ // 1. Fetch template details
53
+ let template;
54
+ try {
55
+ template = await service.serviceTemplateGet(flags.template);
56
+ }
57
+ catch (error) {
58
+ const axiosError = error;
59
+ if (axiosError.response?.status === 404) {
60
+ this.log(chalk.red(`Template not found: ${flags.template}`));
61
+ this.log('Run `hd service templates` to see available templates.');
62
+ this.exit(1);
63
+ return;
64
+ }
65
+ throw error;
66
+ }
67
+ this.log(chalk.blue(`Template: ${chalk.cyan(template.name)} (${template.id})`));
68
+ this.log(chalk.gray(template.description));
69
+ this.log('');
70
+ // 2. Collect parameters
71
+ const parameters = {};
72
+ // Parse --parameter key=value flags first
73
+ const flagParams = {};
74
+ if (flags.parameter) {
75
+ for (const kv of flags.parameter) {
76
+ const eqIdx = kv.indexOf('=');
77
+ if (eqIdx === -1) {
78
+ this.log(chalk.red(`Invalid parameter format: "${kv}". Expected key=value.`));
79
+ this.exit(1);
80
+ return;
81
+ }
82
+ flagParams[kv.substring(0, eqIdx)] = kv.substring(eqIdx + 1);
83
+ }
84
+ }
85
+ for (const param of template.parameters) {
86
+ // Use --parameter flag value if provided
87
+ if (flagParams[param.name] !== undefined) {
88
+ parameters[param.name] = flagParams[param.name];
89
+ continue;
90
+ }
91
+ if (flags['no-interactive']) {
92
+ if (param.default !== undefined) {
93
+ parameters[param.name] = param.default;
94
+ }
95
+ else if (param.required) {
96
+ this.log(chalk.red(`Required parameter "${param.name}" must be provided via --parameter ${param.name}=<value> in non-interactive mode.`));
97
+ this.exit(1);
98
+ return;
99
+ }
100
+ continue;
101
+ }
102
+ let promptMessage = ` ${param.name}`;
103
+ if (param.description)
104
+ promptMessage += ` (${param.description})`;
105
+ if (param.allowed)
106
+ promptMessage += ` [${param.allowed.join(', ')}]`;
107
+ if (param.default !== undefined)
108
+ promptMessage += ` (default: ${param.default})`;
109
+ if (param.required)
110
+ promptMessage += chalk.red(' *');
111
+ const response = await ux.prompt(promptMessage, {
112
+ required: param.required,
113
+ default: param.default,
114
+ });
115
+ if (response) {
116
+ parameters[param.name] = response;
117
+ }
118
+ }
119
+ // 3. Generate slug if not provided
120
+ const serviceSlug = flags.slug || `${template.type}-${flags.stage}-${Date.now().toString(36)}`;
121
+ const serviceName = flags.name || `${template.name} (${flags.stage})`;
122
+ this.log('');
123
+ this.log(chalk.blue('Provisioning:'));
124
+ this.log(` Slug: ${chalk.cyan(serviceSlug)}`);
125
+ this.log(` Template: ${chalk.cyan(template.id)}`);
126
+ this.log(` Stage: ${chalk.cyan(flags.stage)}`);
127
+ if (flags.network)
128
+ this.log(` Network: ${chalk.cyan(flags.network)}`);
129
+ this.log('');
130
+ // 4. Call provisioning API
131
+ const spinner = ora('Submitting provisioning request...').start();
132
+ try {
133
+ const result = await service.serviceProvision({
134
+ templateId: flags.template,
135
+ serviceSlug,
136
+ serviceName,
137
+ stage: flags.stage,
138
+ parameters,
139
+ networkSlug: flags.network,
140
+ });
141
+ spinner.succeed(chalk.green(`Provisioning started: ${result.serviceSlug}`));
142
+ this.log(chalk.gray(` Status: ${result.status}`));
143
+ this.log(chalk.gray(` Log group: ${result.logGroupName}`));
144
+ this.log('');
145
+ // 5. Poll for status
146
+ this.log(chalk.blue('Waiting for provisioning to complete...'));
147
+ const startTime = Date.now();
148
+ while (Date.now() - startTime < PROVISION_TIMEOUT_MS) {
149
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
150
+ try {
151
+ const svc = await service.serviceGet(serviceSlug);
152
+ if (svc.status === 'active') {
153
+ this.log('');
154
+ this.log(chalk.green('Service provisioned successfully!'));
155
+ this.log('');
156
+ this.log(` Slug: ${chalk.cyan(svc.slug)}`);
157
+ this.log(` Endpoint: ${chalk.cyan(svc.endpoint || 'N/A')}`);
158
+ if (svc.port)
159
+ this.log(` Port: ${chalk.cyan(String(svc.port))}`);
160
+ if (svc.credentialSecretArn)
161
+ this.log(` Creds: ${chalk.cyan(svc.credentialSecretArn)}`);
162
+ this.log('');
163
+ this.log(chalk.yellow('Next step:'));
164
+ this.log(chalk.gray(` hd service bind ${serviceSlug} <module> --stage ${flags.stage}`));
165
+ return;
166
+ }
167
+ if (svc.status === 'failed') {
168
+ this.log('');
169
+ this.log(chalk.red('Provisioning failed!'));
170
+ const metadata = svc.metadata || {};
171
+ if (metadata.failureReason) {
172
+ this.log(chalk.red(` Reason: ${metadata.failureReason}`));
173
+ }
174
+ this.exit(1);
175
+ return;
176
+ }
177
+ // Still provisioning
178
+ process.stdout.write(chalk.gray('.'));
179
+ }
180
+ catch {
181
+ // Transient error — keep polling
182
+ process.stdout.write(chalk.yellow('?'));
183
+ }
184
+ }
185
+ // Timeout
186
+ this.log('');
187
+ this.log(chalk.yellow(`Provisioning timeout (${PROVISION_TIMEOUT_MS / 60000} minutes).`));
188
+ this.log(chalk.yellow('The provisioning job may still be running.'));
189
+ this.log(chalk.gray(`Check status: hd service get ${serviceSlug}`));
190
+ this.exit(1);
191
+ }
192
+ catch (error) {
193
+ spinner.fail('Provisioning request failed');
194
+ const axiosError = error;
195
+ const status = axiosError.response?.status;
196
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
197
+ if (status === 400) {
198
+ this.log(chalk.red(`Validation error: ${errorMessage}`));
199
+ }
200
+ else if (status === 404) {
201
+ this.log(chalk.red(`Not found: ${errorMessage}`));
202
+ }
203
+ else if (status === 409) {
204
+ this.log(chalk.red(`Conflict: ${errorMessage}`));
205
+ }
206
+ else {
207
+ this.log(chalk.red(`Error: ${errorMessage}`));
208
+ }
209
+ this.exit(1);
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class ServiceGet extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }