@mateusbmp/backup-manager 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +480 -2
- package/config.example.json +49 -0
- package/config.schema.json +372 -0
- package/dist/cli.js +6863 -0
- package/dist/cli.js.map +1 -0
- package/package.json +86 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MateusBMP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,481 @@
|
|
|
1
|
-
#
|
|
1
|
+
# backup-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/MateusBMP/backup-manager/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@mateusbmp/backup-manager)
|
|
5
|
+
|
|
6
|
+
`bkm` backs up **MariaDB databases** and **Laravel `storage/` directories**, grouped by
|
|
7
|
+
**environment**. You manage everything from one full-screen terminal UI. That includes environments,
|
|
8
|
+
schedules, backups, live jobs, retention and the service. A **user-level systemd service** runs
|
|
9
|
+
the schedules and serves a read-only **HTTP API**. No cron, no root and no sudo are needed.
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Requirements](#requirements)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [First run](#first-run)
|
|
15
|
+
- [The service](#the-service)
|
|
16
|
+
- [Schedules](#schedules)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [The UI](#the-ui)
|
|
19
|
+
- [Jobs](#jobs)
|
|
20
|
+
- [HTTP API](#http-api)
|
|
21
|
+
- [Command line](#command-line)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [For LLMs](#for-llms)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
### Concepts
|
|
30
|
+
|
|
31
|
+
| Concept | Meaning |
|
|
32
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
33
|
+
| **Environment** | The unit of scheduling, retention and locking, e.g. `production` or `staging`. It has any number of projects and databases. |
|
|
34
|
+
| **Project** | A Laravel application. Its `include` directories (default `storage/app`) are archived into `projects/<name>.tar.gz`, minus the `exclude` globs. |
|
|
35
|
+
| **Database** | One `mariadb-dump` output (one or more schemas) saved as `databases/<name>.sql.gz`. Credentials come from a project's `.env` (a `laravel` source) or are set explicitly (a `connection` source). |
|
|
36
|
+
| **Backup** | A directory `<id>/` (`YYYYMMDD-HHMMSS`) with the files above and a `manifest.json` (sizes, SHA-256, durations, errors). |
|
|
37
|
+
| **Job** | One run that backs up one or more environments, in its own process. Its status and an event log live in the state directory. |
|
|
38
|
+
|
|
39
|
+
### On disk
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
~/.local/share/backup-manager/backups/production/ # <backupDir>/<environment>
|
|
43
|
+
├── 20261109-033000/
|
|
44
|
+
│ ├── manifest.json
|
|
45
|
+
│ ├── databases/shop.sql.gz
|
|
46
|
+
│ └── projects/shop.tar.gz
|
|
47
|
+
└── .tmp-20261110-033000/ # in progress; renamed when finished
|
|
48
|
+
|
|
49
|
+
~/.local/state/backup-manager/
|
|
50
|
+
├── jobs/<job-id>.json # status, trigger, pid, timestamps, result
|
|
51
|
+
├── logs/<job-id>.ndjson # structured events (log, item-start/progress/done, job-done)
|
|
52
|
+
├── locks/<environment>.lock # one run per environment at a time
|
|
53
|
+
└── schedule.json # last scheduled run per environment (for catch-up)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A backup is **complete** when every item succeeded and **partial** when some items failed;
|
|
57
|
+
the rest are kept and the errors are recorded. A backup without a manifest is **incomplete**.
|
|
58
|
+
If every item fails, nothing is kept.
|
|
59
|
+
|
|
60
|
+
### Architecture
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
┌──────────────── bkm ────────────────┐
|
|
64
|
+
terminal ────▶ │ UI (React + Ink, full screen) │ systemd --user
|
|
65
|
+
│ doctor (CLI) │ ┌─────────────────────────┐
|
|
66
|
+
└──────────────────┬──────────────────┘ │ bkm daemon │
|
|
67
|
+
▼ │ scheduler (croner) │
|
|
68
|
+
core/services.ts ◀──────────────│ catch-up · reload │
|
|
69
|
+
┌─────────────┬────────────┼──────────────┬─────────┤ HTTP API (Hono) │
|
|
70
|
+
▼ ▼ ▼ ▼ └─────────────────────────┘
|
|
71
|
+
config/store jobs/runner backup/catalog service/manager ──▶ systemctl --user
|
|
72
|
+
(JSON, zod) │ backup/prune
|
|
73
|
+
▼
|
|
74
|
+
bkm __job <id> (detached from the UI, a child of the daemon)
|
|
75
|
+
└─ backup/pipeline ─┬─ mariadb-dump → gzip
|
|
76
|
+
└─ tar.gz (node-tar + globs)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Requirements
|
|
80
|
+
|
|
81
|
+
- Linux with **systemd** (user services) and **Node.js ≥ 22**.
|
|
82
|
+
- MariaDB client tools: `mariadb-dump` (or `mysqldump`) and `mariadb` (or `mysql`).
|
|
83
|
+
Debian/Ubuntu: `apt install mariadb-client`. Arch/Manjaro: `pacman -S mariadb-clients`.
|
|
84
|
+
- A database user that can dump, for example:
|
|
85
|
+
|
|
86
|
+
```sql
|
|
87
|
+
CREATE USER 'backup'@'localhost' IDENTIFIED BY '…';
|
|
88
|
+
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS ON *.* TO 'backup'@'localhost';
|
|
89
|
+
```
|
|
90
|
+
- **Read access** to the Laravel project directories and their `.env` files. The service
|
|
91
|
+
runs as your user, so if the projects belong to `www-data`, add yourself to that group
|
|
92
|
+
(`sudo usermod -aG www-data $USER`, then log in again). `bkm doctor` checks this.
|
|
93
|
+
|
|
94
|
+
## Installation
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install -g @mateusbmp/backup-manager # puts `bkm` on your PATH
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
From source:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/MateusBMP/backup-manager.git
|
|
104
|
+
cd backup-manager
|
|
105
|
+
npm ci && npm run build
|
|
106
|
+
npm install -g .
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## First run
|
|
110
|
+
|
|
111
|
+
Run `bkm` in a terminal. With no config file, the UI walks you through two steps:
|
|
112
|
+
|
|
113
|
+
1. **Settings:** where backups go and how long to keep them. This writes
|
|
114
|
+
`~/.config/backup-manager/config.json`, which keeps only the values you changed.
|
|
115
|
+
2. **First environment:** name, an optional schedule, then projects and databases. You can
|
|
116
|
+
also add them later. Choose **Install** when asked, and the service starts running your
|
|
117
|
+
schedules.
|
|
118
|
+
|
|
119
|
+
`bkm doctor` checks everything afterwards: config, tools, directories, project access,
|
|
120
|
+
database connections and the service.
|
|
121
|
+
|
|
122
|
+
## The service
|
|
123
|
+
|
|
124
|
+
The service is a **user-level** systemd unit, `~/.config/systemd/user/bkm.service`. Setting it
|
|
125
|
+
up never uses sudo or root. You install and manage it from the **Service** screen in `bkm`
|
|
126
|
+
(onboarding also offers to install it):
|
|
127
|
+
|
|
128
|
+
| Action | What it does |
|
|
129
|
+
| ------ | ------------ |
|
|
130
|
+
| **Install / update** | Writes the unit, runs `daemon-reload` and `enable --now`, and enables lingering. If the unit is outdated, it is rewritten and the service restarted. Running it again when nothing changed does nothing. |
|
|
131
|
+
| **Start / Stop / Restart** | `systemctl --user start\|stop\|restart bkm` |
|
|
132
|
+
| **Reload config** | `systemctl --user reload bkm` (SIGHUP) |
|
|
133
|
+
| **Uninstall** | `disable --now`, removes the unit, `daemon-reload`. Config, backups and job history are kept. |
|
|
134
|
+
|
|
135
|
+
The same unit can be handled with the usual tools:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
systemctl --user status bkm
|
|
139
|
+
systemctl --user reload bkm # re-read config.json (it also reloads by itself when the file changes)
|
|
140
|
+
systemctl --user restart bkm
|
|
141
|
+
journalctl --user -u bkm -f # scheduler and job log
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Lingering:** **Install / update** runs `loginctl enable-linger $USER`. This lets the service start at
|
|
145
|
+
boot and keep running after you log out. On most systems this needs no root. If your
|
|
146
|
+
system refuses it, the install still succeeds, with a warning that the service only runs
|
|
147
|
+
while you are logged in.
|
|
148
|
+
- **Upgrades:** the unit stores absolute paths to `node` and `bkm`. If either one moves
|
|
149
|
+
(an nvm upgrade or a reinstall), `bkm doctor` and the UI flag the unit as **outdated**.
|
|
150
|
+
Choose **Install / update** on the Service screen to rewrite it.
|
|
151
|
+
- **Stopping:** on `systemctl --user stop bkm`, the daemon stops scheduling and waits up to
|
|
152
|
+
5 minutes for running jobs. Then it cancels them; an unfinished backup is discarded.
|
|
153
|
+
|
|
154
|
+
## Schedules
|
|
155
|
+
|
|
156
|
+
Each environment can have a `schedule`, which is a cron expression. Without one, it is
|
|
157
|
+
backed up only on demand.
|
|
158
|
+
|
|
159
|
+
| Expression | Meaning |
|
|
160
|
+
| ---------------- | ------------------------------------------ |
|
|
161
|
+
| `30 3 * * *` | daily at 03:30 |
|
|
162
|
+
| `0 */6 * * *` | every 6 hours |
|
|
163
|
+
| `0 2 * * 1-5` | weekdays at 02:00 |
|
|
164
|
+
| `0 4 1 * *` | monthly on the 1st at 04:00 |
|
|
165
|
+
| `*/30 * * * * *` | every 30 seconds (6 fields: seconds first) |
|
|
166
|
+
|
|
167
|
+
- Times are in `settings.timezone`, or the system timezone when it is not set. The editor
|
|
168
|
+
shows a description and the next three runs as you type.
|
|
169
|
+
- **Catch-up:** if the service was down when a run was due (for example, the machine was
|
|
170
|
+
off), that run happens once when the service starts again.
|
|
171
|
+
- An environment never runs twice at the same time. A run that is due while the previous one
|
|
172
|
+
is still going is skipped and logged. `settings.maxConcurrentJobs` limits how many
|
|
173
|
+
environments run at once.
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
The config is one JSON file, normally edited from the UI.
|
|
178
|
+
|
|
179
|
+
- **Location:** `--config` (or `-c`), then `$BKM_CONFIG`, then `$XDG_CONFIG_HOME/backup-manager/config.json`.
|
|
180
|
+
- **Saving:** the UI writes it atomically with mode 0600 and keeps the previous version as
|
|
181
|
+
`config.json.bak`. It writes only non-default values. If the file changed on disk since it
|
|
182
|
+
was loaded, the UI reloads it instead of overwriting.
|
|
183
|
+
- **Editor support:** `config.schema.json` gives editors autocompletion and validation through
|
|
184
|
+
the `$schema` key. See [`config.example.json`](config.example.json) for a complete example.
|
|
185
|
+
- **Errors** are reported by JSON path, e.g. `environments[0].schedule: invalid cron expression`.
|
|
186
|
+
- **Passwords:** prefer `passwordFile` over `password`. Either way, credentials reach
|
|
187
|
+
`mariadb-dump` only through a temporary 0600 option file, never on the command line.
|
|
188
|
+
|
|
189
|
+
Every key (generated from the schema):
|
|
190
|
+
|
|
191
|
+
<!-- config-reference:start -->
|
|
192
|
+
| Key | Type | Default | Description |
|
|
193
|
+
| --- | --- | --- | --- |
|
|
194
|
+
| `version` | `1` | `1` | Config format version. |
|
|
195
|
+
| `settings` | object | | Global settings. |
|
|
196
|
+
| `settings.backupDir` | string | | Root directory for backups. Default: $XDG_DATA_HOME/backup-manager/backups. |
|
|
197
|
+
| `settings.stateDir` | string | | Directory for jobs, logs, locks and schedule state. Default: $XDG_STATE_HOME/backup-manager. |
|
|
198
|
+
| `settings.compressLevel` | integer 1–9 | `6` | gzip compression level for dumps and archives (1 fastest … 9 smallest). |
|
|
199
|
+
| `settings.autoPrune` | boolean | `true` | Apply the retention policy after every successful backup. |
|
|
200
|
+
| `settings.timezone` | string | | IANA timezone for schedules, e.g. 'America/Sao_Paulo'. Default: the system timezone. |
|
|
201
|
+
| `settings.maxConcurrentJobs` | integer 1–16 | `1` | Maximum scheduled jobs the daemon runs at the same time. |
|
|
202
|
+
| `retention` | object | | Retention policy. A backup is kept when either rule keeps it. |
|
|
203
|
+
| `retention.keepLast` | integer ≥ 0 | `7` | Keep the newest N complete backups (0 disables this rule). |
|
|
204
|
+
| `retention.keepDays` | integer ≥ 0 | `30` | Keep complete backups younger than N days (0 disables this rule). |
|
|
205
|
+
| `api` | object | | Read-only HTTP API served by `bkm daemon`. |
|
|
206
|
+
| `api.enabled` | boolean | `true` | Serve the read-only HTTP API from the daemon. |
|
|
207
|
+
| `api.host` | string | `"127.0.0.1"` | Address to bind. |
|
|
208
|
+
| `api.port` | integer 1–65535 | `8080` | TCP port. |
|
|
209
|
+
| `api.token` | string | | Bearer token required by every route except /health, /docs and /openapi.json. |
|
|
210
|
+
| `api.tokenFile` | string | | File containing the bearer token (alternative to token). |
|
|
211
|
+
| `environments[]` | object[] | | Environments, in display order. |
|
|
212
|
+
| `environments[].name` | string | | Environment name: lowercase letters, digits, '-' and '_'. |
|
|
213
|
+
| `environments[].schedule` | string | | Cron expression (5 or 6 fields) for automatic backups. Omit for manual only. |
|
|
214
|
+
| `environments[].backupDir` | string | | Backup directory used as-is. Default: <settings.backupDir>/<name>. |
|
|
215
|
+
| `environments[].retention` | object | | Per-environment retention override. |
|
|
216
|
+
| `environments[].retention.keepLast` | integer ≥ 0 | | Overrides the global keepLast. |
|
|
217
|
+
| `environments[].retention.keepDays` | integer ≥ 0 | | Overrides the global keepDays. |
|
|
218
|
+
| `environments[].projects[]` | object[] | | Laravel projects whose storage is archived. |
|
|
219
|
+
| `environments[].projects[].name` | string | | Project name: lowercase letters, digits, '-' and '_'. |
|
|
220
|
+
| `environments[].projects[].path` | string | | Absolute path to the Laravel project root (the directory with artisan). |
|
|
221
|
+
| `environments[].projects[].include` | string[] | `["storage/app"]` | Directories to archive, relative to the project root. |
|
|
222
|
+
| `environments[].projects[].exclude` | string[] | `["storage/framework/{cache,sessions,views}/**","storage/logs/**","**/.gitignore"]` | Glob patterns (relative to the project root) to leave out. 'dir/**' keeps the empty directory. |
|
|
223
|
+
| `environments[].databases[]` | object[] | | Databases dumped with mariadb-dump. |
|
|
224
|
+
| `environments[].databases[].name` | string | | Database name: lowercase letters, digits, '-' and '_'. |
|
|
225
|
+
| `environments[].databases[].source` (type `laravel`) | object | | Read the connection from a Laravel project's .env; any field set here wins. |
|
|
226
|
+
| `environments[].databases[].source.type` | `"laravel"` | | |
|
|
227
|
+
| `environments[].databases[].source.project` | string | | Project (in the same environment) whose .env provides DB_* values. |
|
|
228
|
+
| `environments[].databases[].source.host` | string | | Server host. |
|
|
229
|
+
| `environments[].databases[].source.port` | integer 1–65535 | | Server port. |
|
|
230
|
+
| `environments[].databases[].source.socket` | string | | Unix socket path (instead of host/port). |
|
|
231
|
+
| `environments[].databases[].source.user` | string | | User name. |
|
|
232
|
+
| `environments[].databases[].source.password` | string | | Password. Prefer passwordFile. |
|
|
233
|
+
| `environments[].databases[].source.passwordFile` | string | | File containing the password (first line). |
|
|
234
|
+
| `environments[].databases[].source.database` | string | | Overrides DB_DATABASE. |
|
|
235
|
+
| `environments[].databases[].source` (type `connection`) | object | | An explicit MariaDB/MySQL connection. |
|
|
236
|
+
| `environments[].databases[].source.type` | `"connection"` | | |
|
|
237
|
+
| `environments[].databases[].source.host` | string | | Server host. |
|
|
238
|
+
| `environments[].databases[].source.port` | integer 1–65535 | | Server port. |
|
|
239
|
+
| `environments[].databases[].source.socket` | string | | Unix socket path (instead of host/port). |
|
|
240
|
+
| `environments[].databases[].source.user` | string | | User name. |
|
|
241
|
+
| `environments[].databases[].source.password` | string | | Password. Prefer passwordFile. |
|
|
242
|
+
| `environments[].databases[].source.passwordFile` | string | | File containing the password (first line). |
|
|
243
|
+
| `environments[].databases[].source.schemas` | string[] | | Schemas (databases) to dump into one file. |
|
|
244
|
+
| `environments[].databases[].extraArgs` | string[] | `[]` | Extra arguments for mariadb-dump, e.g. ['--ignore-table=app.sessions']. |
|
|
245
|
+
<!-- config-reference:end -->
|
|
246
|
+
|
|
247
|
+
## The UI
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
▓▒░ backup-manager ░▒▓ host · service ● active · 2 envs · 12:04
|
|
251
|
+
╭──────────────────╮ ╭─ Environments ───────────────────────────────────────────────╮
|
|
252
|
+
│ ● Dashboard │ │ ❯ production 2 db · 1 proj 14 bkp 3.1 GiB ✔ 2h ⏱ 03:30 │
|
|
253
|
+
│ Environments │ │ staging 1 proj 6 bkp 220 MiB ⚠ 1d ⏱ — │
|
|
254
|
+
│ Backups │ ╰──────────────────────────────────────────────────────────────╯
|
|
255
|
+
│ Jobs 1 │ ╭─ Running ────────────────────────────────────────────────────╮
|
|
256
|
+
│ New backup │ │ ⠹ production · database shop ████████░░░░ 1/2 41s schedule│
|
|
257
|
+
│ Prune │ ╰──────────────────────────────────────────────────────────────╯
|
|
258
|
+
│ Service │ ╭─ Recent backups ─────────────────────────────────────────────╮
|
|
259
|
+
│ Doctor │ │ ✔ production 20261109-033000 complete 1.6 GiB 9h ago │
|
|
260
|
+
│ Settings │ ╰──────────────────────────────────────────────────────────────╯
|
|
261
|
+
╰──────────────────╯
|
|
262
|
+
↑↓ move ⏎ backups r run now n new backup e environments tab pane ? help q quit
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
| Screen | What you can do |
|
|
266
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
267
|
+
| **Dashboard** | Environments at a glance, running jobs with progress, recent backups. `r` runs the selected environment now. |
|
|
268
|
+
| **Environments** | `a` add, `⏎` edit, `d` delete (backups on disk are kept), `b` its backups, `r` run now. |
|
|
269
|
+
| **Environment editor** | Name, schedule with a live preview, backup dir and retention overrides, plus its projects and databases. Projects detect `artisan`. Databases have a **Test connection** action. |
|
|
270
|
+
| **Backups** | Per environment (`←→`), filter by status (`f`), `⏎` details with every item, checksum and error. |
|
|
271
|
+
| **Jobs** | Every run with its trigger (`ui`, `schedule`, `catch-up`). `⏎` follows one live, `x` stops it. |
|
|
272
|
+
| **New backup** | Pick environments, then items, then confirm. The job starts detached and opens its live view. |
|
|
273
|
+
| **Prune** | A dry run of the retention policy. `p` applies it after confirmation. |
|
|
274
|
+
| **Service** | State, linger and outdated badges; install, start, stop, restart, reload and uninstall; journal tail; next runs; API address. |
|
|
275
|
+
| **Doctor** | The same checks as `bkm doctor`; `r` runs them again. |
|
|
276
|
+
| **Settings** | Storage, retention, scheduler and API settings. |
|
|
277
|
+
|
|
278
|
+
**Keys:** `↑↓`/`j k` move · `g G`/`Home End` first/last · `PgUp PgDn` · `⏎` open ·
|
|
279
|
+
`Esc` back · `Tab` switches between the sidebar and the content (in forms, it moves to the next
|
|
280
|
+
field) · `1`–`9` jump to a menu entry · `n` new backup · `e` environments · `s` service ·
|
|
281
|
+
`Ctrl-S` saves a form · `?` help · `q` quit.
|
|
282
|
+
|
|
283
|
+
Below 90 columns the sidebar becomes a tab bar, and below 60×16 the UI asks you to resize.
|
|
284
|
+
`NO_COLOR` is honored.
|
|
285
|
+
|
|
286
|
+
## Jobs
|
|
287
|
+
|
|
288
|
+
Every backup runs in its own process (`bkm __job <id>`):
|
|
289
|
+
|
|
290
|
+
- **Started from the UI**, the job is **detached** in its own session. Quitting the UI or
|
|
291
|
+
losing the SSH connection does not stop it. Open `bkm` again and **Jobs** shows it, live.
|
|
292
|
+
- **Started by the service**, the job is a normal child of the daemon. It stays in the
|
|
293
|
+
service's cgroup, so `systemctl --user stop bkm` shuts it down cleanly.
|
|
294
|
+
- **Stopping** a job (`x`) signals its whole process group, so `mariadb-dump` stops too, and the
|
|
295
|
+
unfinished backup is discarded. If a job's process disappears, the job is marked failed.
|
|
296
|
+
- After each **complete** backup, the retention policy is applied (`settings.autoPrune`).
|
|
297
|
+
|
|
298
|
+
## HTTP API
|
|
299
|
+
|
|
300
|
+
The service serves a read-only API, at `http://127.0.0.1:8080` by default (see `api.*`).
|
|
301
|
+
OpenAPI docs are at `/docs`.
|
|
302
|
+
|
|
303
|
+
| Route | Returns |
|
|
304
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
305
|
+
| `GET /health` | status, version, uptime, next scheduled runs |
|
|
306
|
+
| `GET /environments` | summaries: counts, sizes, last backup, `nextRun`, running |
|
|
307
|
+
| `GET /environments/{env}` | the summary plus `backupDir` and effective retention |
|
|
308
|
+
| `GET /environments/{env}/projects` | name, path, include, exclude |
|
|
309
|
+
| `GET /environments/{env}/databases` | name, type, project, host, port, socket, schemas (**never** user, password or extra args) |
|
|
310
|
+
| `GET /environments/{env}/backups?status=&limit=` | backups, newest first |
|
|
311
|
+
| `GET /environments/{env}/backups/{id}` | one backup with every item from its manifest |
|
|
312
|
+
| `GET /jobs?limit=` | recent jobs |
|
|
313
|
+
|
|
314
|
+
When `api.token` or `api.tokenFile` is set, every route except `/health`, `/docs` and
|
|
315
|
+
`/openapi.json` needs `Authorization: Bearer <token>`. If the API binds to a non-loopback
|
|
316
|
+
address without a token, the service logs a warning.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
curl -s http://127.0.0.1:8080/environments | jq
|
|
320
|
+
curl -s -H "Authorization: Bearer $(cat ~/.config/backup-manager/api.token)" http://127.0.0.1:8080/jobs
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Command line
|
|
324
|
+
|
|
325
|
+
The CLI is deliberately small: everything else, including installing the service, is in the UI.
|
|
326
|
+
|
|
327
|
+
| Command | Purpose |
|
|
328
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------- |
|
|
329
|
+
| `bkm` | Full-screen UI. Without a terminal (TTY), it prints help and exits 2. |
|
|
330
|
+
| `bkm doctor [--no-db]` | Check everything; prints ✔/!/✘ and exits 1 when something fails. |
|
|
331
|
+
| `bkm daemon` | The service process. systemd starts it; you can also run it in the foreground to debug. |
|
|
332
|
+
|
|
333
|
+
Global options: `-c/--config <path>`, `-v/--version`, `-h/--help`.
|
|
334
|
+
Exit codes: `0` ok, `1` failure, `2` config or usage error (also when there is no config yet),
|
|
335
|
+
`3` the environment is already being backed up.
|
|
336
|
+
|
|
337
|
+
## Development
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm ci # also installs the git hooks (husky)
|
|
341
|
+
npm run dev # run the CLI from source (tsx), e.g. npm run dev -- doctor
|
|
342
|
+
npm test # unit + UI tests (vitest)
|
|
343
|
+
npm run coverage # with the coverage gate (lines/statements/functions ≥ 85%, branches ≥ 75%)
|
|
344
|
+
npm run test:integration # real MariaDB, see below
|
|
345
|
+
npm run lint # Biome (format + lint); `npm run format` fixes
|
|
346
|
+
npm run typecheck # tsc --noEmit (TypeScript 7, strict)
|
|
347
|
+
npm run schema # regenerate config.schema.json and the config table in this README
|
|
348
|
+
npm run build # tsup → dist/cli.js
|
|
349
|
+
npm run verify # schema check + typecheck + lint + coverage + build (same as pre-push and CI)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- **Git hooks:** pre-commit runs `biome check --write` on staged files. Pre-push runs
|
|
353
|
+
`npm run verify` and blocks the push if it fails.
|
|
354
|
+
- **CI** runs on pushes to `main`, on PRs and manually. Jobs: lint/types/schema; tests on
|
|
355
|
+
Node 22, 24 and 26; integration against a MariaDB 11 service; and pack + install +
|
|
356
|
+
CLI smoke test (`--help`, `--version`, `doctor`, a short `daemon` run). On pushes to `main`,
|
|
357
|
+
a final **stage** job runs once all of those pass (see *Releasing*). **CodeQL** (`security-extended`)
|
|
358
|
+
scans the TypeScript and the workflows on pushes, PRs and weekly.
|
|
359
|
+
- **Dependabot** updates npm packages and actions weekly (dev tools in one PR) and groups
|
|
360
|
+
security updates into one PR per ecosystem. `@types/node` majors are bumped by hand, since
|
|
361
|
+
they track the oldest supported Node. Dependabot malware alerts are enabled in the repo settings.
|
|
362
|
+
- **Integration tests** run only when a database is configured:
|
|
363
|
+
`BKM_IT_DB_HOST`/`PORT`/`USER`/`PASSWORD` (or `BKM_IT_DB_SOCKET`) and `BKM_IT_DB_NAME`
|
|
364
|
+
(default `bkm_it`). `BKM_IT_DB_CREATE=1` creates the schema and a table first. Without it,
|
|
365
|
+
the schema is only read, so the tests are safe against a local server, e.g.
|
|
366
|
+
`BKM_IT_DB_SOCKET=/run/mysqld/mysqld.sock BKM_IT_DB_NAME=test npm run test:integration`.
|
|
367
|
+
- Tests never touch real systemd (a fake runner is injected) or real databases (a fake
|
|
368
|
+
`mariadb-dump` is put on `PATH`), except the opt-in integration tests.
|
|
369
|
+
|
|
370
|
+
### Releasing
|
|
371
|
+
|
|
372
|
+
The package is published to npm as
|
|
373
|
+
[`@mateusbmp/backup-manager`](https://www.npmjs.com/package/@mateusbmp/backup-manager).
|
|
374
|
+
CI never makes a version public on its own. It **stages** the release with
|
|
375
|
+
`npm stage publish`, and you approve it with 2FA.
|
|
376
|
+
|
|
377
|
+
1. Bump `version` in `package.json` (e.g. `npm version patch --no-git-tag-version`, since CI
|
|
378
|
+
creates the tag), commit and push to `main`.
|
|
379
|
+
2. When every CI job is green, the `stage` job in `ci.yml` runs `npm run release:check`
|
|
380
|
+
(`scripts/release-check.ts`). It stages only when the version is not on npm yet, has no
|
|
381
|
+
`vX.Y.Z` git tag, and is greater than the current `latest`. A prerelease such as
|
|
382
|
+
`1.2.0-beta.1` gets the `next` dist-tag. The job stages the exact tarball that passed the smoke
|
|
383
|
+
test, with provenance. It then pushes the `vX.Y.Z` tag, which marks the version as staged so
|
|
384
|
+
later pushes skip it, and creates a **draft** GitHub Release with generated notes. Pushes that
|
|
385
|
+
don't change the version stage nothing.
|
|
386
|
+
3. Review and approve:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
npm stage list @mateusbmp/backup-manager # find the stage id
|
|
390
|
+
npm stage download <stage-id> # optional: inspect the tarball
|
|
391
|
+
npm stage approve <stage-id> # publishes it (asks for 2FA)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Then publish the draft GitHub Release. To drop a staged version instead, run
|
|
395
|
+
`npm stage reject <stage-id>`, delete the draft release and the `vX.Y.Z` tag, and bump again
|
|
396
|
+
(or push a fix and re-run CI).
|
|
397
|
+
|
|
398
|
+
One-time setup:
|
|
399
|
+
|
|
400
|
+
1. Publish the first version manually, because npm only stages versions of packages that
|
|
401
|
+
already exist: `npm login && npm publish` from a clean `main` checkout (`prepack` builds
|
|
402
|
+
`dist/`). Until then, the `stage` job skips with a notice.
|
|
403
|
+
2. Create a GitHub environment named `npm` (Settings → Environments).
|
|
404
|
+
3. On npmjs.com → package → Settings → **Trusted publishing**, add GitHub Actions with owner
|
|
405
|
+
`MateusBMP`, repository `backup-manager`, workflow `ci.yml` and environment `npm`. Allow only
|
|
406
|
+
**stage publish**, not publish. From the terminal:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npm trust github @mateusbmp/backup-manager --file ci.yml \
|
|
410
|
+
--repository MateusBMP/backup-manager --environment npm --allow-stage-publish
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Under publishing access, choose "Require two-factor authentication and disallow tokens".
|
|
414
|
+
CI then needs no stored secret.
|
|
415
|
+
|
|
416
|
+
## For LLMs
|
|
417
|
+
|
|
418
|
+
`CLAUDE.md`, `AGENTS.md`, `.claude/` and `.agents/` are **gitignored**. Each developer or AI
|
|
419
|
+
agent recreates them locally, and **this section is the source of truth** for them. When
|
|
420
|
+
commands or architecture change, update this section in the same commit.
|
|
421
|
+
|
|
422
|
+
### Recreating the agent environment
|
|
423
|
+
|
|
424
|
+
If you are an AI coding agent (Claude Code, Codex, or another `AGENTS.md`-compatible tool)
|
|
425
|
+
starting work on this repository:
|
|
426
|
+
|
|
427
|
+
1. Read this README completely.
|
|
428
|
+
2. Bootstrap and verify that the project is healthy: `npm ci && npm run verify`.
|
|
429
|
+
3. Create the local instructions file from the "Agent context" block below:
|
|
430
|
+
- **Claude Code:** write `CLAUDE.md`.
|
|
431
|
+
- **Other agents:** write `AGENTS.md`.
|
|
432
|
+
4. Optionally allow the routine commands in `.claude/settings.local.json`, e.g.
|
|
433
|
+
`Bash(npm test:*)`, `Bash(npm run verify:*)`, `Bash(npx vitest:*)`, `Bash(npx biome:*)`, `Bash(npx tsc:*)`.
|
|
434
|
+
|
|
435
|
+
### Agent context (paste into CLAUDE.md / AGENTS.md)
|
|
436
|
+
|
|
437
|
+
```markdown
|
|
438
|
+
# backup-manager — agent context
|
|
439
|
+
|
|
440
|
+
TypeScript (Node ≥22, ESM, strict) tool `bkm`: backs up MariaDB databases (mariadb-dump → gzip)
|
|
441
|
+
and Laravel storage dirs (tar.gz) per environment. Full-screen React + Ink 8 UI; a user-level
|
|
442
|
+
systemd service (`bkm daemon`) runs cron schedules (croner) and a read-only Hono API.
|
|
443
|
+
JSON config validated by zod. npm.
|
|
444
|
+
|
|
445
|
+
## Commands
|
|
446
|
+
|
|
447
|
+
- Install: `npm ci` · Run from source: `npm run dev -- <args>` · Build: `npm run build`
|
|
448
|
+
- Test: `npm test` · coverage gate: `npm run coverage` · one file: `npx vitest run tests/core/jobs.test.ts`
|
|
449
|
+
- Integration (real MariaDB): `BKM_IT_DB_SOCKET=/run/mysqld/mysqld.sock BKM_IT_DB_NAME=test npm run test:integration`
|
|
450
|
+
- Lint/format: `npm run lint` / `npm run format` (Biome) · Types: `npm run typecheck`
|
|
451
|
+
- Schema + README config table: `npm run schema` (CI fails if stale)
|
|
452
|
+
- Full gate (pre-push and CI): `npm run verify`
|
|
453
|
+
- Release: bump `version` and push to `main`; once all jobs pass, the `stage` job in `ci.yml` runs `npm stage publish` for `@mateusbmp/backup-manager` (`scripts/release-check.ts` decides), tags `vX.Y.Z` and drafts a GitHub Release; a maintainer runs `npm stage approve` (README "Releasing")
|
|
454
|
+
|
|
455
|
+
## Architecture
|
|
456
|
+
|
|
457
|
+
- `src/cli.tsx` → `src/cli/program.ts` (commander): default = UI, `doctor`, `daemon`, hidden `__job <id>`. Service install/uninstall is UI-only (Service screen → `ServiceManager`).
|
|
458
|
+
- `src/core/` never imports React. `core/services.ts` is the only facade used by the CLI, UI, API and daemon.
|
|
459
|
+
- Config: `core/config/schema.ts` (zod, single source of truth) → `load.ts` (errors as JSON paths) → `store.ts` (the ONLY writer: minimal, atomic, 0600, `.bak`, mtime conflict check). `defaults.ts` resolves paths and retention.
|
|
460
|
+
- Backups: `core/backup/pipeline.ts` (one env; `.tmp-<id>` → manifest → rename; auto-prune), `mariadb.ts`, `archive.ts`, `catalog.ts`, `retention.ts` (pure), `prune.ts`, `lock.ts`.
|
|
461
|
+
- Jobs: `core/jobs/store.ts` (JSON status), `events.ts` (NDJSON), `runner.ts` (`startJob` spawns `bkm __job`: detached from the UI, a normal child from the daemon; `executeJob` runs it).
|
|
462
|
+
- Daemon: `core/daemon/` (scheduler, catch-up via `schedule.json`, reload on file change or SIGHUP, graceful stop) + `src/api/server.ts`.
|
|
463
|
+
- systemd: `core/service/` — every systemctl/journalctl/loginctl call goes through the injectable `CommandRunner` in `systemctl.ts`, always `--user`, never sudo, never a shell.
|
|
464
|
+
- UI: `src/ui/App.tsx` (frame, routing, global keys, toasts, modals), `screens/`, `components/` (Form, TextField, ScrollList/ScrollView, Panel), `hooks/` (usePolling, useJobEvents). `ui/print/` renders doctor output for the CLI.
|
|
465
|
+
|
|
466
|
+
## Conventions
|
|
467
|
+
|
|
468
|
+
- Ink: colors only from `ui/theme.ts`; gate every `useInput` with `isActive` (`useScreenActive()`); forms take the keyboard via `useCaptureKeys`; size from the App context (`width`/`height`), not ad-hoc.
|
|
469
|
+
- Tests: vitest + ink-testing-library (100 columns; send a no-op key after mount and wait before keys; match frames after `stripVTControlCharacters`, since CI forces color). Temp dirs via `tests/helpers.ts`; fake `mariadb-dump` (`installFakeDump`) and fake systemd (`fakeSystemd`).
|
|
470
|
+
- Every behavior change needs tests; keep README (and this block) in sync in the same change.
|
|
471
|
+
|
|
472
|
+
## Safety
|
|
473
|
+
|
|
474
|
+
- Secrets never in argv, logs or API responses (credentials go through a 0600 `--defaults-extra-file`).
|
|
475
|
+
- Tests never call the real systemctl or real databases (except opt-in integration tests).
|
|
476
|
+
- Never commit `config.json`, `*.pw`, tokens, or CLAUDE.md/AGENTS.md. Do not commit or push unless asked; never `--no-verify`.
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## License
|
|
480
|
+
|
|
481
|
+
[MIT](LICENSE) © 2026 MateusBMP
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/MateusBMP/backup-manager/main/config.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"settings": {
|
|
5
|
+
"backupDir": "~/backups",
|
|
6
|
+
"compressLevel": 9,
|
|
7
|
+
"timezone": "America/Sao_Paulo"
|
|
8
|
+
},
|
|
9
|
+
"retention": { "keepLast": 14, "keepDays": 30 },
|
|
10
|
+
"api": { "host": "127.0.0.1", "port": 8080, "tokenFile": "~/.config/backup-manager/api.token" },
|
|
11
|
+
"environments": [
|
|
12
|
+
{
|
|
13
|
+
"name": "production",
|
|
14
|
+
"schedule": "30 3 * * *",
|
|
15
|
+
"projects": [
|
|
16
|
+
{
|
|
17
|
+
"name": "shop",
|
|
18
|
+
"path": "/var/www/shop",
|
|
19
|
+
"include": ["storage/app"],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"storage/framework/{cache,sessions,views}/**",
|
|
22
|
+
"storage/logs/**",
|
|
23
|
+
"**/.gitignore"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"databases": [
|
|
28
|
+
{ "name": "shop", "source": { "type": "laravel", "project": "shop" } },
|
|
29
|
+
{
|
|
30
|
+
"name": "reporting",
|
|
31
|
+
"source": {
|
|
32
|
+
"type": "connection",
|
|
33
|
+
"host": "127.0.0.1",
|
|
34
|
+
"user": "backup",
|
|
35
|
+
"passwordFile": "~/.config/backup-manager/reporting.pw",
|
|
36
|
+
"schemas": ["reports", "reports_archive"]
|
|
37
|
+
},
|
|
38
|
+
"extraArgs": ["--ignore-table=reports.cache"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "staging",
|
|
44
|
+
"retention": { "keepLast": 3 },
|
|
45
|
+
"projects": [{ "name": "shop", "path": "/var/www/staging-shop" }],
|
|
46
|
+
"databases": [{ "name": "shop", "source": { "type": "laravel", "project": "shop" } }]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|