@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 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
- # Temporary Holding Version
1
+ # backup-manager
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/MateusBMP/backup-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/MateusBMP/backup-manager/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@mateusbmp/backup-manager)](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
+ }