@heronlabs/terminal-cli 0.0.3 → 1.0.0

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # ðŸŠķ terminal-cli — Heron CLI (`hcli`)
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@heronlabs/terminal-cli.svg)](https://www.npmjs.com/package/@heronlabs/terminal-cli)
4
+ [![license](https://img.shields.io/npm/l/@heronlabs/terminal-cli.svg)](./LICENSE)
3
5
  [![CI](https://github.com/heronlabs/terminal-cli/actions/workflows/ci-cli.yml/badge.svg)](https://github.com/heronlabs/terminal-cli/actions/workflows/ci-cli.yml)
4
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.x-blue.svg)](https://www.typescriptlang.org/)
5
7
  [![NestJS](https://img.shields.io/badge/NestJS-11.x-e0234e.svg)](https://nestjs.com/)
@@ -14,6 +16,7 @@ AWS S3, and Luxon. Designed to run as a scheduled cron container on EasyPanel.
14
16
  ## Table of Contents
15
17
 
16
18
  - [Why This Project](#why-this-project)
19
+ - [Install](#install)
17
20
  - [Quick Start](#quick-start)
18
21
  - [Commands](#commands)
19
22
  - [Configuration](#configuration)
@@ -32,6 +35,25 @@ Scheduled, type-safe database backups with a single binary:
32
35
  - **Cron-ready containers** — purpose-built Dockerfiles run a backup on start and every 12 hours.
33
36
  - **100% tested** — v8 coverage + Stryker mutation testing, both at 100% thresholds.
34
37
 
38
+ ## Install
39
+
40
+ `hcli` is a global CLI binary. Install it globally to put `hcli` on your `PATH`:
41
+
42
+ ```bash
43
+ npm i -g @heronlabs/terminal-cli
44
+ # or: pnpm add -g @heronlabs/terminal-cli
45
+ ```
46
+
47
+ Then run any command:
48
+
49
+ ```bash
50
+ hcli version
51
+ hcli psql-backup
52
+ ```
53
+
54
+ You also need the database client tools for the engine you back up: `pg_dump` /
55
+ `psql` for PostgreSQL, `mysqldump` / `mysql` for MySQL.
56
+
35
57
  ## Quick Start
36
58
 
37
59
  ### Prerequisites
@@ -43,7 +65,7 @@ Scheduled, type-safe database backups with a single binary:
43
65
  | `pg_dump` / `psql` | — | PostgreSQL backup/restore |
44
66
  | `mysqldump` / `mysql` | — | MySQL backup/restore |
45
67
 
46
- ### Installation
68
+ ### From source (development)
47
69
 
48
70
  ```bash
49
71
  git clone https://github.com/heronlabs/terminal-cli.git
@@ -104,11 +126,7 @@ All configuration comes from environment variables (see [.env.example](./.env.ex
104
126
 
105
127
  | Variable | Required | Description |
106
128
  |---|---|---|
107
- | `DB_HOST` | ✅ | Database host |
108
- | `DB_PORT` | ✅ | Database port |
109
- | `DB_NAME` | ✅ | Database name |
110
- | `DB_USER` | ✅ | Database user |
111
- | `DB_PASSWORD` | ✅ | Database password |
129
+ | `DB_URL` | ✅ | Connection URL (`postgres://`/`mysql://user:pass@host:port/dbname`) or an AWS SSM Parameter Store ARN resolved via [`@heronlabs/env-ssm`](https://www.npmjs.com/package/@heronlabs/env-ssm) |
112
130
  | `AWS_S3_BUCKET_NAME` | for S3 | Destination bucket for remote backups |
113
131
  | `AWS_REGION` | for S3 | AWS region |
114
132
  | `AWS_ACCESS_KEY_ID` | for S3 | AWS credentials (or use an instance role) |
@@ -6,7 +6,7 @@ class BackupService {
6
6
  logger;
7
7
  s3StorageService;
8
8
  async run(local, filename) {
9
- const result = this.dump(filename);
9
+ const result = await this.dump(filename);
10
10
  if (!result.ok) {
11
11
  this.logger.error(result.error.message);
12
12
  return;
@@ -13,7 +13,7 @@ class RollupService {
13
13
  return;
14
14
  }
15
15
  }
16
- const result = this.restore(filename);
16
+ const result = await this.restore(filename);
17
17
  if (!result.ok) {
18
18
  this.logger.error(result.error.message);
19
19
  return;
@@ -20,8 +20,8 @@ let MysqlBackupService = class MysqlBackupService extends backup_service_1.Backu
20
20
  logger;
21
21
  environmentService;
22
22
  s3StorageService;
23
- dump(filename) {
24
- const { host, port, name, user, password } = this.environmentService.database;
23
+ async dump(filename) {
24
+ const { host, port, name, user, password } = await this.environmentService.database();
25
25
  const timestamp = luxon_1.DateTime.utc().toFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
26
26
  const backupFileName = filename ?? `${name}-${timestamp}.sql.gz`;
27
27
  try {
@@ -19,8 +19,8 @@ let MysqlRollupService = class MysqlRollupService extends rollup_service_1.Rollu
19
19
  logger;
20
20
  environmentService;
21
21
  s3StorageService;
22
- restore(backupFileName) {
23
- const { host, port, name, user, password } = this.environmentService.database;
22
+ async restore(backupFileName) {
23
+ const { host, port, name, user, password } = await this.environmentService.database();
24
24
  try {
25
25
  (0, child_process_1.execSync)('set -o pipefail; gunzip -c "$BACKUP_FILE" | mariadb -u "$DB_USER" -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME"', {
26
26
  env: {
@@ -20,8 +20,8 @@ let PsqlBackupService = class PsqlBackupService extends backup_service_1.BackupS
20
20
  logger;
21
21
  environmentService;
22
22
  s3StorageService;
23
- dump(filename) {
24
- const { host, port, name, user, password } = this.environmentService.database;
23
+ async dump(filename) {
24
+ const { host, port, name, user, password } = await this.environmentService.database();
25
25
  const timestamp = luxon_1.DateTime.utc().toFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
26
26
  const backupFileName = filename ?? `${name}-${timestamp}.sql.gz`;
27
27
  try {
@@ -19,8 +19,8 @@ let PsqlRollupService = class PsqlRollupService extends rollup_service_1.RollupS
19
19
  logger;
20
20
  environmentService;
21
21
  s3StorageService;
22
- restore(backupFileName) {
23
- const { host, port, name, user, password } = this.environmentService.database;
22
+ async restore(backupFileName) {
23
+ const { host, port, name, user, password } = await this.environmentService.database();
24
24
  try {
25
25
  (0, child_process_1.execSync)('set -o pipefail; gunzip -c "$BACKUP_FILE" | psql', {
26
26
  env: {
@@ -7,13 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.EnvironmentModule = exports.environmentModule = void 0;
10
+ const env_ssm_1 = require("@heronlabs/env-ssm");
10
11
  const common_1 = require("@nestjs/common");
11
12
  const config_1 = require("@nestjs/config");
12
13
  const environment_service_1 = require("./services/environment-service");
13
14
  exports.environmentModule = {
14
15
  providers: [environment_service_1.EnvironmentService],
15
16
  exports: [environment_service_1.EnvironmentService],
16
- imports: [config_1.ConfigModule],
17
+ imports: [config_1.ConfigModule, env_ssm_1.SsmConfigModule],
17
18
  };
18
19
  let EnvironmentModule = class EnvironmentModule {
19
20
  };
@@ -10,30 +10,47 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.EnvironmentService = void 0;
13
+ const env_ssm_1 = require("@heronlabs/env-ssm");
13
14
  const common_1 = require("@nestjs/common");
14
15
  const config_1 = require("@nestjs/config");
15
16
  let EnvironmentService = class EnvironmentService {
16
17
  configService;
17
- get database() {
18
- return {
19
- host: this.configService.getOrThrow('DB_HOST'),
20
- port: this.configService.getOrThrow('DB_PORT'),
21
- name: this.configService.getOrThrow('DB_NAME'),
22
- user: this.configService.getOrThrow('DB_USER'),
23
- password: this.configService.getOrThrow('DB_PASSWORD'),
18
+ ssmConfigService;
19
+ async database() {
20
+ const databaseUrl = await this.ssmConfigService.getOrThrow('DB_URL');
21
+ let url;
22
+ try {
23
+ url = new URL(databaseUrl);
24
+ }
25
+ catch {
26
+ throw new Error('Invalid DB_URL');
27
+ }
28
+ const connection = {
29
+ host: url.hostname,
30
+ port: url.port,
31
+ name: url.pathname.slice(1),
32
+ user: decodeURIComponent(url.username),
33
+ password: decodeURIComponent(url.password),
24
34
  };
35
+ const missing = ['host', 'name', 'user'].filter(field => connection[field] === '');
36
+ if (missing.length > 0) {
37
+ throw new Error(`Invalid DB_URL: missing ${missing.join(', ')}`);
38
+ }
39
+ return connection;
25
40
  }
26
41
  get storage() {
27
42
  return {
28
43
  bucketName: this.configService.getOrThrow('AWS_S3_BUCKET_NAME'),
29
44
  };
30
45
  }
31
- constructor(configService) {
46
+ constructor(configService, ssmConfigService) {
32
47
  this.configService = configService;
48
+ this.ssmConfigService = ssmConfigService;
33
49
  }
34
50
  };
35
51
  exports.EnvironmentService = EnvironmentService;
36
52
  exports.EnvironmentService = EnvironmentService = __decorate([
37
53
  (0, common_1.Injectable)(),
38
- __metadata("design:paramtypes", [config_1.ConfigService])
54
+ __metadata("design:paramtypes", [config_1.ConfigService,
55
+ env_ssm_1.SsmConfigService])
39
56
  ], EnvironmentService);
package/package.json CHANGED
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@aws-sdk/client-s3": "^3.1045.0",
11
+ "@heronlabs/env-ssm": "^1.0.0",
11
12
  "@nestjs/common": "^11.1.18",
12
13
  "@nestjs/config": "^4.0.4",
13
14
  "@nestjs/core": "^11.1.18",
@@ -71,6 +72,7 @@
71
72
  "@swc/core"
72
73
  ],
73
74
  "overrides": {
75
+ "qs@<6.15.2": ">=6.15.2",
74
76
  "tmp@<0.2.6": ">=0.2.6"
75
77
  }
76
78
  },
@@ -92,5 +94,5 @@
92
94
  "test:mutation": "stryker run stryker.conf.json",
93
95
  "test:unit": "vitest run"
94
96
  },
95
- "version": "0.0.3"
97
+ "version": "1.0.0"
96
98
  }