ui-syncup 0.3.6 → 0.3.11-alpha
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 +94 -0
- package/dist/index.js +391 -25
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ui-syncup CLI
|
|
2
|
+
|
|
3
|
+
Self-host [UI SyncUp](https://github.com/BYKHD/ui-syncup) with a single command. No infrastructure knowledge required.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Docker](https://docs.docker.com/get-docker/) ≥ 24
|
|
8
|
+
- Node.js ≥ 20 (only needed to run the CLI — the app itself runs in Docker)
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
mkdir my-syncup && cd my-syncup
|
|
14
|
+
npx ui-syncup init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The wizard downloads `compose.yml`, walks you through service configuration, and starts the stack.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### Setup & Lifecycle
|
|
22
|
+
|
|
23
|
+
| Command | Description |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `init` | Guided first-time setup wizard |
|
|
26
|
+
| `start` | Start the stack (reads `COMPOSE_PROFILES` from `.env`) |
|
|
27
|
+
| `stop` | Stop gracefully — data is preserved |
|
|
28
|
+
| `restart [service]` | Restart all services or a single one |
|
|
29
|
+
| `remove` | Remove containers (`--volumes` to also wipe all data) |
|
|
30
|
+
|
|
31
|
+
### Observability
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `status` | Show container states, health, and app URL |
|
|
36
|
+
| `logs [service]` | Tail last 200 lines (`-F` to stream live) |
|
|
37
|
+
| `doctor` | Validate env vars, health endpoint, and disk space |
|
|
38
|
+
| `open` | Open the app in your default browser |
|
|
39
|
+
|
|
40
|
+
### Maintenance
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `upgrade` | Pull latest image and restart (migrations apply automatically) |
|
|
45
|
+
| `backup` | Dump PostgreSQL + MinIO to a timestamped `.tar.gz` |
|
|
46
|
+
| `restore <archive>` | Restore from a backup archive |
|
|
47
|
+
|
|
48
|
+
## Usage Examples
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# First-time setup
|
|
52
|
+
npx ui-syncup init
|
|
53
|
+
|
|
54
|
+
# Day-to-day
|
|
55
|
+
ui-syncup status
|
|
56
|
+
ui-syncup logs -F # stream all logs
|
|
57
|
+
ui-syncup logs app -F # stream app logs only
|
|
58
|
+
ui-syncup restart app # restart just the app container
|
|
59
|
+
|
|
60
|
+
# Upgrades
|
|
61
|
+
ui-syncup upgrade
|
|
62
|
+
|
|
63
|
+
# Backup & restore
|
|
64
|
+
ui-syncup backup -o ~/backups
|
|
65
|
+
ui-syncup restore ~/backups/ui-syncup-backup-2026-03-19T12-00.tar.gz
|
|
66
|
+
|
|
67
|
+
# Teardown
|
|
68
|
+
ui-syncup remove # keep data volumes
|
|
69
|
+
ui-syncup remove --volumes # wipe everything
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Bundled Services (Docker Compose profiles)
|
|
73
|
+
|
|
74
|
+
`init` lets you choose which services to bundle. Your selection is saved as `COMPOSE_PROFILES` in `.env` so subsequent `start`/`upgrade` commands pick it up automatically.
|
|
75
|
+
|
|
76
|
+
| Profile | Service | Use when |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `db` | PostgreSQL 15 | No external database |
|
|
79
|
+
| `cache` | Redis 7 | No external Redis/Upstash |
|
|
80
|
+
| `storage` | MinIO | No external S3/R2/Backblaze |
|
|
81
|
+
|
|
82
|
+
## Backup Details
|
|
83
|
+
|
|
84
|
+
`backup` only exports data for active profiles:
|
|
85
|
+
|
|
86
|
+
- **PostgreSQL** (`db` profile) — `pg_dumpall` → `postgres.sql`
|
|
87
|
+
- **MinIO** (`storage` profile) — volume tar → `minio_data.tar.gz`
|
|
88
|
+
- Redis is intentionally excluded (cache — not persistent state)
|
|
89
|
+
|
|
90
|
+
Output is a single `.tar.gz` archive you can store offsite.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
9
12
|
var __copyProps = (to, from, except, desc) => {
|
|
10
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
14
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,6 +26,42 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
26
|
mod
|
|
24
27
|
));
|
|
25
28
|
|
|
29
|
+
// package.json
|
|
30
|
+
var require_package = __commonJS({
|
|
31
|
+
"package.json"(exports2, module2) {
|
|
32
|
+
module2.exports = {
|
|
33
|
+
name: "ui-syncup",
|
|
34
|
+
version: "0.3.11-alpha",
|
|
35
|
+
description: "Self-host UI SyncUp with a single command",
|
|
36
|
+
bin: {
|
|
37
|
+
"ui-syncup": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
files: [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
scripts: {
|
|
43
|
+
build: "tsup",
|
|
44
|
+
dev: "tsup --watch"
|
|
45
|
+
},
|
|
46
|
+
dependencies: {
|
|
47
|
+
commander: "^12.0.0",
|
|
48
|
+
"@inquirer/prompts": "^8.0.0",
|
|
49
|
+
chalk: "^5.3.0",
|
|
50
|
+
ora: "^8.0.0"
|
|
51
|
+
},
|
|
52
|
+
devDependencies: {
|
|
53
|
+
tsup: "^8.0.0",
|
|
54
|
+
typescript: "^5.0.0",
|
|
55
|
+
"@types/node": "^20.0.0"
|
|
56
|
+
},
|
|
57
|
+
engines: {
|
|
58
|
+
node: ">=20"
|
|
59
|
+
},
|
|
60
|
+
license: "MIT"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
26
65
|
// index.ts
|
|
27
66
|
var import_commander = require("commander");
|
|
28
67
|
|
|
@@ -33,13 +72,36 @@ var import_prompts = require("@inquirer/prompts");
|
|
|
33
72
|
|
|
34
73
|
// src/lib/ui.ts
|
|
35
74
|
var import_chalk = __toESM(require("chalk"));
|
|
75
|
+
var pipe = import_chalk.default.white("\u2502");
|
|
36
76
|
var ui = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
77
|
+
banner: (version3) => {
|
|
78
|
+
const title = ` \u2732 UI SyncUp v${version3} `;
|
|
79
|
+
const tagline = ` A visual feedback and issue tracking platform `;
|
|
80
|
+
const width = Math.max(title.length, tagline.length);
|
|
81
|
+
const pad = (s) => s + " ".repeat(width - s.length);
|
|
82
|
+
console.log(import_chalk.default.white("\u2554" + "\u2550".repeat(width) + "\u2557"));
|
|
83
|
+
console.log(
|
|
84
|
+
import_chalk.default.white("\u2551") + " " + import_chalk.default.white("\u2732") + " " + import_chalk.default.bold.white("UI SyncUp") + " " + import_chalk.default.dim(`v${version3}`) + " ".repeat(width - title.length + 2) + import_chalk.default.white("\u2551")
|
|
85
|
+
);
|
|
86
|
+
console.log(import_chalk.default.white("\u2560" + "\u2500".repeat(width) + "\u2563"));
|
|
87
|
+
console.log(
|
|
88
|
+
import_chalk.default.white("\u2551") + import_chalk.default.dim(pad(tagline)) + import_chalk.default.white("\u2551")
|
|
89
|
+
);
|
|
90
|
+
console.log(import_chalk.default.white("\u255A" + "\u2550".repeat(width) + "\u255D"));
|
|
91
|
+
},
|
|
92
|
+
header: (msg) => {
|
|
93
|
+
console.log(" ");
|
|
94
|
+
console.log(import_chalk.default.cyan("\u2699\uFE0E") + " " + import_chalk.default.bold.white(msg));
|
|
95
|
+
console.log(pipe);
|
|
96
|
+
},
|
|
97
|
+
step: (n, total, msg) => {
|
|
98
|
+
console.log(import_chalk.default.magenta("\u25C6") + " " + import_chalk.default.bold.magenta(`Step ${n} of ${total}`));
|
|
99
|
+
console.log(pipe + " " + import_chalk.default.white(msg));
|
|
100
|
+
},
|
|
101
|
+
info: (msg) => console.log(pipe + " \u{1F680} " + import_chalk.default.blue(msg)),
|
|
102
|
+
success: (msg) => console.log(pipe + " \u2728 " + import_chalk.default.green(msg)),
|
|
103
|
+
warn: (msg) => console.log(pipe + " \u26A0\uFE0F " + import_chalk.default.yellow(msg)),
|
|
104
|
+
error: (msg) => console.error(pipe + " \u{1F6A8} " + import_chalk.default.red(msg))
|
|
43
105
|
};
|
|
44
106
|
|
|
45
107
|
// src/lib/env.ts
|
|
@@ -89,18 +151,20 @@ function isDockerRunning() {
|
|
|
89
151
|
return false;
|
|
90
152
|
}
|
|
91
153
|
}
|
|
92
|
-
function runCompose(composeFile, args, profiles = []) {
|
|
154
|
+
function runCompose(composeFile, args, profiles = [], quiet = false) {
|
|
93
155
|
const profileFlags = profiles.flatMap((p) => ["--profile", p]);
|
|
94
156
|
const cmd = ["docker", "compose", "-f", composeFile, ...profileFlags, ...args];
|
|
95
|
-
const result = (0, import_node_child_process.spawnSync)(cmd[0], cmd.slice(1), { stdio: "inherit" });
|
|
157
|
+
const result = (0, import_node_child_process.spawnSync)(cmd[0], cmd.slice(1), { stdio: quiet ? "pipe" : "inherit" });
|
|
96
158
|
return { success: result.status === 0 };
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
// src/commands/init.ts
|
|
162
|
+
var { version } = require_package();
|
|
100
163
|
var COMPOSE_URL = "https://raw.githubusercontent.com/BYKHD/ui-syncup/main/docker/compose.yml";
|
|
101
164
|
var ENV_EXAMPLE_URL2 = "https://raw.githubusercontent.com/BYKHD/ui-syncup/main/.env.example";
|
|
102
165
|
async function initCommand() {
|
|
103
|
-
ui.
|
|
166
|
+
ui.banner(version);
|
|
167
|
+
ui.header("Setup Wizard \u{1FA84}");
|
|
104
168
|
ui.step(1, 6, "Checking Docker...");
|
|
105
169
|
if (!isDockerRunning()) {
|
|
106
170
|
ui.error("Docker is not running. Please start Docker and try again.");
|
|
@@ -252,31 +316,323 @@ async function initCommand() {
|
|
|
252
316
|
}
|
|
253
317
|
}
|
|
254
318
|
|
|
319
|
+
// src/commands/start.ts
|
|
320
|
+
var import_node_fs3 = require("fs");
|
|
321
|
+
async function startCommand(composeFile) {
|
|
322
|
+
ui.header("UI SyncUp \u2014 Start");
|
|
323
|
+
if (!isDockerRunning()) {
|
|
324
|
+
ui.error("Docker is not running.");
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const profiles = (0, import_node_fs3.existsSync)(".env") ? (parseEnv(".env")["COMPOSE_PROFILES"] ?? "").split(",").filter(Boolean) : [];
|
|
328
|
+
ui.info("Starting stack...");
|
|
329
|
+
const result = runCompose(composeFile, ["up", "-d"], profiles);
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
ui.error("Failed to start stack \u2014 check logs with: ui-syncup logs");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
ui.success("Stack is running.");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/commands/stop.ts
|
|
338
|
+
async function stopCommand(composeFile) {
|
|
339
|
+
ui.header("UI SyncUp \u2014 Stop");
|
|
340
|
+
if (!isDockerRunning()) {
|
|
341
|
+
ui.error("Docker is not running.");
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
ui.info("Stopping stack...");
|
|
345
|
+
const result = runCompose(composeFile, ["stop"]);
|
|
346
|
+
if (!result.success) {
|
|
347
|
+
ui.error("Failed to stop stack");
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
ui.success("Stack stopped. Data preserved \u2014 run: ui-syncup start to resume.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/restart.ts
|
|
354
|
+
async function restartCommand(composeFile, service) {
|
|
355
|
+
ui.header("UI SyncUp \u2014 Restart");
|
|
356
|
+
if (!isDockerRunning()) {
|
|
357
|
+
ui.error("Docker is not running.");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
const args = service ? ["restart", service] : ["restart"];
|
|
361
|
+
ui.info(service ? `Restarting ${service}...` : "Restarting all services...");
|
|
362
|
+
const result = runCompose(composeFile, args);
|
|
363
|
+
if (!result.success) {
|
|
364
|
+
ui.error("Restart failed");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
ui.success("Restart complete.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/commands/status.ts
|
|
371
|
+
var import_node_child_process3 = require("child_process");
|
|
372
|
+
var import_node_fs4 = require("fs");
|
|
373
|
+
async function statusCommand(composeFile) {
|
|
374
|
+
if (!isDockerRunning()) {
|
|
375
|
+
ui.error("Docker is not running.");
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
if (!(0, import_node_fs4.existsSync)(composeFile)) {
|
|
379
|
+
ui.error(`Compose file not found: ${composeFile}. Run: ui-syncup init`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
(0, import_node_child_process3.spawnSync)("docker", ["compose", "-f", composeFile, "ps"], { stdio: "inherit" });
|
|
383
|
+
const vars = (0, import_node_fs4.existsSync)(".env") ? parseEnv(".env") : {};
|
|
384
|
+
const port = vars["PORT"] || "3000";
|
|
385
|
+
const appUrl = vars["NEXT_PUBLIC_APP_URL"] || `http://localhost:${port}`;
|
|
386
|
+
console.log("");
|
|
387
|
+
ui.info(`App \u2192 ${appUrl}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/commands/logs.ts
|
|
391
|
+
var import_node_child_process4 = require("child_process");
|
|
392
|
+
var import_node_fs5 = require("fs");
|
|
393
|
+
async function logsCommand(composeFile, service, follow) {
|
|
394
|
+
if (!isDockerRunning()) {
|
|
395
|
+
ui.error("Docker is not running.");
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
if (!(0, import_node_fs5.existsSync)(composeFile)) {
|
|
399
|
+
ui.error(`Compose file not found: ${composeFile}. Run: ui-syncup init`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
const args = ["compose", "-f", composeFile, "logs", "--tail=200"];
|
|
403
|
+
if (follow) args.push("--follow");
|
|
404
|
+
if (service) args.push(service);
|
|
405
|
+
(0, import_node_child_process4.spawnSync)("docker", args, { stdio: "inherit" });
|
|
406
|
+
}
|
|
407
|
+
|
|
255
408
|
// src/commands/upgrade.ts
|
|
409
|
+
var import_ora = __toESM(require("ora"));
|
|
256
410
|
async function upgradeCommand(composeFile) {
|
|
257
411
|
ui.header("UI SyncUp \u2014 Upgrade");
|
|
258
412
|
if (!isDockerRunning()) {
|
|
259
413
|
ui.error("Docker is not running.");
|
|
260
414
|
process.exit(1);
|
|
261
415
|
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (!
|
|
265
|
-
|
|
416
|
+
const pull = (0, import_ora.default)("Pulling latest image...").start();
|
|
417
|
+
const pullResult = runCompose(composeFile, ["pull"], [], true);
|
|
418
|
+
if (!pullResult.success) {
|
|
419
|
+
pull.fail("docker compose pull failed");
|
|
266
420
|
process.exit(1);
|
|
267
421
|
}
|
|
268
|
-
|
|
269
|
-
const up =
|
|
270
|
-
|
|
271
|
-
|
|
422
|
+
pull.succeed("Latest image pulled");
|
|
423
|
+
const up = (0, import_ora.default)("Restarting stack (migrations run automatically)...").start();
|
|
424
|
+
const upResult = runCompose(composeFile, ["up", "-d", "--remove-orphans"], [], true);
|
|
425
|
+
if (!upResult.success) {
|
|
426
|
+
up.fail("Stack restart failed \u2014 check logs with: ui-syncup logs");
|
|
272
427
|
process.exit(1);
|
|
273
428
|
}
|
|
274
|
-
|
|
429
|
+
up.succeed("Upgrade complete. Migrations applied.");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/commands/backup.ts
|
|
433
|
+
var import_node_child_process5 = require("child_process");
|
|
434
|
+
var import_node_fs6 = require("fs");
|
|
435
|
+
var import_node_path = require("path");
|
|
436
|
+
var import_ora2 = __toESM(require("ora"));
|
|
437
|
+
async function backupCommand(composeFile, outputDir) {
|
|
438
|
+
ui.header("UI SyncUp \u2014 Backup");
|
|
439
|
+
if (!isDockerRunning()) {
|
|
440
|
+
ui.error("Docker is not running.");
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
444
|
+
const label = `ui-syncup-backup-${timestamp}`;
|
|
445
|
+
const backupDir = (0, import_node_path.resolve)(outputDir, label);
|
|
446
|
+
(0, import_node_fs6.mkdirSync)(backupDir, { recursive: true });
|
|
447
|
+
const vars = (0, import_node_fs6.existsSync)(".env") ? parseEnv(".env") : {};
|
|
448
|
+
const profiles = (vars["COMPOSE_PROFILES"] ?? "").split(",").filter(Boolean);
|
|
449
|
+
let backedUp = false;
|
|
450
|
+
if (profiles.includes("db")) {
|
|
451
|
+
const spinner = (0, import_ora2.default)("Dumping PostgreSQL...").start();
|
|
452
|
+
try {
|
|
453
|
+
const dump = (0, import_node_child_process5.execSync)(
|
|
454
|
+
`docker compose -f ${composeFile} exec -T postgres pg_dumpall -U postgres`,
|
|
455
|
+
{ encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 }
|
|
456
|
+
);
|
|
457
|
+
(0, import_node_fs6.writeFileSync)(`${backupDir}/postgres.sql`, dump);
|
|
458
|
+
spinner.succeed("PostgreSQL dump saved");
|
|
459
|
+
backedUp = true;
|
|
460
|
+
} catch {
|
|
461
|
+
spinner.fail("PostgreSQL backup failed \u2014 is the db container running?");
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
ui.info("Skipping PostgreSQL (db profile not active)");
|
|
465
|
+
}
|
|
466
|
+
if (profiles.includes("storage")) {
|
|
467
|
+
const spinner = (0, import_ora2.default)("Exporting MinIO volume...").start();
|
|
468
|
+
const result = (0, import_node_child_process5.spawnSync)("docker", [
|
|
469
|
+
"run",
|
|
470
|
+
"--rm",
|
|
471
|
+
"-v",
|
|
472
|
+
"ui-syncup_minio_data:/volume",
|
|
473
|
+
"-v",
|
|
474
|
+
`${backupDir}:/backup`,
|
|
475
|
+
"alpine",
|
|
476
|
+
"tar",
|
|
477
|
+
"czf",
|
|
478
|
+
"/backup/minio_data.tar.gz",
|
|
479
|
+
"-C",
|
|
480
|
+
"/volume",
|
|
481
|
+
"."
|
|
482
|
+
], { stdio: "pipe" });
|
|
483
|
+
if (result.status === 0) {
|
|
484
|
+
spinner.succeed("MinIO volume exported");
|
|
485
|
+
backedUp = true;
|
|
486
|
+
} else {
|
|
487
|
+
spinner.fail("MinIO backup failed \u2014 is the storage profile running?");
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
ui.info("Skipping MinIO (storage profile not active)");
|
|
491
|
+
}
|
|
492
|
+
if (!backedUp) {
|
|
493
|
+
ui.warn("No data services active. Set COMPOSE_PROFILES=db,storage in .env to enable backups.");
|
|
494
|
+
(0, import_node_fs6.rmSync)(backupDir, { recursive: true });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const archiving = (0, import_ora2.default)("Archiving...").start();
|
|
498
|
+
const archivePath = (0, import_node_path.resolve)(outputDir, `${label}.tar.gz`);
|
|
499
|
+
(0, import_node_child_process5.spawnSync)("tar", ["-czf", archivePath, "-C", (0, import_node_path.resolve)(outputDir), label], { stdio: "pipe" });
|
|
500
|
+
(0, import_node_fs6.rmSync)(backupDir, { recursive: true });
|
|
501
|
+
archiving.succeed(`Backup saved \u2192 ${archivePath}`);
|
|
502
|
+
console.log("");
|
|
503
|
+
ui.info(`To restore: ui-syncup restore ${archivePath}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/commands/restore.ts
|
|
507
|
+
var import_node_child_process6 = require("child_process");
|
|
508
|
+
var import_node_fs7 = require("fs");
|
|
509
|
+
var import_node_path2 = require("path");
|
|
510
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
511
|
+
var import_ora3 = __toESM(require("ora"));
|
|
512
|
+
async function restoreCommand(composeFile, archivePath) {
|
|
513
|
+
ui.header("UI SyncUp \u2014 Restore");
|
|
514
|
+
if (!isDockerRunning()) {
|
|
515
|
+
ui.error("Docker is not running.");
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
const resolvedArchive = (0, import_node_path2.resolve)(archivePath);
|
|
519
|
+
if (!(0, import_node_fs7.existsSync)(resolvedArchive)) {
|
|
520
|
+
ui.error(`Archive not found: ${resolvedArchive}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
ui.warn("Restore will overwrite current data. The app container will be stopped briefly.");
|
|
524
|
+
const confirmed = await (0, import_prompts2.confirm)({ message: "Continue with restore?" });
|
|
525
|
+
if (!confirmed) {
|
|
526
|
+
ui.info("Aborted.");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const tmpDir = `/tmp/ui-syncup-restore-${Date.now()}`;
|
|
530
|
+
(0, import_node_fs7.mkdirSync)(tmpDir, { recursive: true });
|
|
531
|
+
const extractSpinner = (0, import_ora3.default)("Extracting archive...").start();
|
|
532
|
+
const extractResult = (0, import_node_child_process6.spawnSync)("tar", ["-xzf", resolvedArchive, "-C", tmpDir]);
|
|
533
|
+
if (extractResult.status !== 0) {
|
|
534
|
+
extractSpinner.fail("Failed to extract archive");
|
|
535
|
+
(0, import_node_fs7.rmSync)(tmpDir, { recursive: true });
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
extractSpinner.succeed("Archive extracted");
|
|
539
|
+
const entries = (0, import_node_fs7.readdirSync)(tmpDir);
|
|
540
|
+
const backupDir = (0, import_node_path2.join)(tmpDir, entries[0]);
|
|
541
|
+
const vars = (0, import_node_fs7.existsSync)(".env") ? parseEnv(".env") : {};
|
|
542
|
+
const profiles = (vars["COMPOSE_PROFILES"] ?? "").split(",").filter(Boolean);
|
|
543
|
+
const stopSpinner = (0, import_ora3.default)("Stopping app container...").start();
|
|
544
|
+
runCompose(composeFile, ["stop", "app"], [], true);
|
|
545
|
+
stopSpinner.succeed("App container stopped");
|
|
546
|
+
const sqlFile = (0, import_node_path2.join)(backupDir, "postgres.sql");
|
|
547
|
+
if ((0, import_node_fs7.existsSync)(sqlFile) && profiles.includes("db")) {
|
|
548
|
+
const spinner = (0, import_ora3.default)("Restoring PostgreSQL...").start();
|
|
549
|
+
const result = (0, import_node_child_process6.spawnSync)(
|
|
550
|
+
"bash",
|
|
551
|
+
["-c", `docker compose -f ${composeFile} exec -T postgres psql -U postgres < "${sqlFile}"`],
|
|
552
|
+
{ stdio: "pipe" }
|
|
553
|
+
);
|
|
554
|
+
if (result.status === 0) {
|
|
555
|
+
spinner.succeed("PostgreSQL restored");
|
|
556
|
+
} else {
|
|
557
|
+
spinner.fail("PostgreSQL restore failed \u2014 check: ui-syncup logs postgres");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const minioArchive = (0, import_node_path2.join)(backupDir, "minio_data.tar.gz");
|
|
561
|
+
if ((0, import_node_fs7.existsSync)(minioArchive) && profiles.includes("storage")) {
|
|
562
|
+
const spinner = (0, import_ora3.default)("Restoring MinIO volume...").start();
|
|
563
|
+
(0, import_node_child_process6.spawnSync)("docker", [
|
|
564
|
+
"run",
|
|
565
|
+
"--rm",
|
|
566
|
+
"-v",
|
|
567
|
+
"ui-syncup_minio_data:/volume",
|
|
568
|
+
"-v",
|
|
569
|
+
`${backupDir}:/backup`,
|
|
570
|
+
"alpine",
|
|
571
|
+
"sh",
|
|
572
|
+
"-c",
|
|
573
|
+
"tar xzf /backup/minio_data.tar.gz -C /volume"
|
|
574
|
+
], { stdio: "pipe" });
|
|
575
|
+
spinner.succeed("MinIO volume restored");
|
|
576
|
+
}
|
|
577
|
+
(0, import_node_fs7.rmSync)(tmpDir, { recursive: true });
|
|
578
|
+
const restartSpinner = (0, import_ora3.default)("Restarting stack...").start();
|
|
579
|
+
runCompose(composeFile, ["up", "-d"], [], true);
|
|
580
|
+
restartSpinner.succeed("Stack restarted");
|
|
581
|
+
console.log("");
|
|
582
|
+
ui.success("Restore complete.");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/commands/open.ts
|
|
586
|
+
var import_node_child_process7 = require("child_process");
|
|
587
|
+
var import_node_fs8 = require("fs");
|
|
588
|
+
async function openCommand() {
|
|
589
|
+
const vars = (0, import_node_fs8.existsSync)(".env") ? parseEnv(".env") : {};
|
|
590
|
+
const port = vars["PORT"] || "3000";
|
|
591
|
+
const url = vars["NEXT_PUBLIC_APP_URL"] || `http://localhost:${port}`;
|
|
592
|
+
ui.info(`Opening ${url}...`);
|
|
593
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
594
|
+
try {
|
|
595
|
+
(0, import_node_child_process7.execSync)(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
596
|
+
} catch {
|
|
597
|
+
ui.warn(`Could not open browser automatically. Visit: ${url}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/commands/remove.ts
|
|
602
|
+
var import_prompts3 = require("@inquirer/prompts");
|
|
603
|
+
async function removeCommand(composeFile, withVolumes) {
|
|
604
|
+
ui.header("UI SyncUp \u2014 Remove");
|
|
605
|
+
if (!isDockerRunning()) {
|
|
606
|
+
ui.error("Docker is not running.");
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
if (withVolumes) {
|
|
610
|
+
ui.warn("This permanently deletes ALL data: database, storage, and cache volumes.");
|
|
611
|
+
} else {
|
|
612
|
+
ui.warn("This stops and removes containers. Data volumes will be preserved.");
|
|
613
|
+
}
|
|
614
|
+
const confirmed = await (0, import_prompts3.confirm)({
|
|
615
|
+
message: withVolumes ? "Delete all data and remove the stack?" : "Remove the stack (keep data volumes)?"
|
|
616
|
+
});
|
|
617
|
+
if (!confirmed) {
|
|
618
|
+
ui.info("Aborted.");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const args = ["down"];
|
|
622
|
+
if (withVolumes) args.push("--volumes");
|
|
623
|
+
const result = runCompose(composeFile, args);
|
|
624
|
+
if (!result.success) {
|
|
625
|
+
ui.error("Remove failed");
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
ui.success(
|
|
629
|
+
withVolumes ? "Stack and all data removed." : "Stack removed. Data volumes preserved. Run: ui-syncup start to restart."
|
|
630
|
+
);
|
|
275
631
|
}
|
|
276
632
|
|
|
277
633
|
// src/commands/doctor.ts
|
|
278
|
-
var
|
|
279
|
-
var
|
|
634
|
+
var import_node_child_process8 = require("child_process");
|
|
635
|
+
var import_node_fs9 = require("fs");
|
|
280
636
|
async function doctorCommand() {
|
|
281
637
|
ui.header("UI SyncUp \u2014 Doctor");
|
|
282
638
|
let allGood = true;
|
|
@@ -288,7 +644,7 @@ async function doctorCommand() {
|
|
|
288
644
|
allGood = false;
|
|
289
645
|
}
|
|
290
646
|
ui.info("Checking .env required variables...");
|
|
291
|
-
if (!(0,
|
|
647
|
+
if (!(0, import_node_fs9.existsSync)(".env")) {
|
|
292
648
|
ui.error(".env not found. Run: npx ui-syncup init");
|
|
293
649
|
allGood = false;
|
|
294
650
|
} else {
|
|
@@ -305,9 +661,9 @@ async function doctorCommand() {
|
|
|
305
661
|
}
|
|
306
662
|
ui.info("Checking /api/health...");
|
|
307
663
|
try {
|
|
308
|
-
const vars = (0,
|
|
664
|
+
const vars = (0, import_node_fs9.existsSync)(".env") ? parseEnv(".env") : {};
|
|
309
665
|
const appUrl = vars["NEXT_PUBLIC_APP_URL"] || "http://localhost:3000";
|
|
310
|
-
const raw = (0,
|
|
666
|
+
const raw = (0, import_node_child_process8.execSync)(`curl -sf "${appUrl}/api/health"`, {
|
|
311
667
|
encoding: "utf-8",
|
|
312
668
|
timeout: 5e3
|
|
313
669
|
});
|
|
@@ -323,7 +679,7 @@ async function doctorCommand() {
|
|
|
323
679
|
}
|
|
324
680
|
ui.info("Checking disk space...");
|
|
325
681
|
try {
|
|
326
|
-
const dfOut = (0,
|
|
682
|
+
const dfOut = (0, import_node_child_process8.execSync)("df -k . | awk 'NR==2{print $4}'", {
|
|
327
683
|
encoding: "utf-8"
|
|
328
684
|
}).trim();
|
|
329
685
|
const freeGB = parseInt(dfOut, 10) / 1024 / 1024;
|
|
@@ -346,9 +702,19 @@ async function doctorCommand() {
|
|
|
346
702
|
}
|
|
347
703
|
|
|
348
704
|
// index.ts
|
|
705
|
+
var { version: version2 } = require_package();
|
|
349
706
|
var DEFAULT_COMPOSE = "compose.yml";
|
|
350
|
-
var program = new import_commander.Command().name("ui-syncup").description("Self-host UI SyncUp with a single command").version(
|
|
707
|
+
var program = new import_commander.Command().name("ui-syncup").description("Self-host UI SyncUp with a single command").version(version2);
|
|
351
708
|
program.command("init").description("Guided setup: download compose file, configure services, start the stack").action(initCommand);
|
|
709
|
+
program.command("start").description("Start the stack (reads COMPOSE_PROFILES from .env)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action(({ file }) => startCommand(file));
|
|
710
|
+
program.command("stop").description("Stop the stack gracefully (data is preserved)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action(({ file }) => stopCommand(file));
|
|
711
|
+
program.command("restart [service]").description("Restart all services or a single one (app|postgres|redis|minio)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action((service, { file }) => restartCommand(file, service));
|
|
712
|
+
program.command("status").description("Show container states, health, and app URL").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action(({ file }) => statusCommand(file));
|
|
713
|
+
program.command("logs [service]").description("Tail logs for all services or a single one (app|postgres|redis|minio)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).option("-F, --follow", "Stream logs (Ctrl+C to stop)", false).action((service, { file, follow }) => logsCommand(file, service, follow));
|
|
352
714
|
program.command("upgrade").description("Pull latest image and restart the stack (migrations run automatically)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action(({ file }) => upgradeCommand(file));
|
|
715
|
+
program.command("backup").description("Dump PostgreSQL and MinIO to a timestamped .tar.gz archive").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).option("-o, --output <dir>", "Directory to write the archive into", ".").action(({ file, output }) => backupCommand(file, output));
|
|
716
|
+
program.command("restore <archive>").description("Restore from a backup archive (stops app briefly during restore)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).action((archive, { file }) => restoreCommand(file, archive));
|
|
717
|
+
program.command("open").description("Open the app in your default browser").action(openCommand);
|
|
718
|
+
program.command("remove").description("Remove containers (add --volumes to also wipe all data)").option("-f, --file <path>", "Path to compose file", DEFAULT_COMPOSE).option("--volumes", "Also delete all data volumes (irreversible)", false).action(({ file, volumes }) => removeCommand(file, volumes));
|
|
353
719
|
program.command("doctor").description("Validate environment, service health, and disk space").action(doctorCommand);
|
|
354
720
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui-syncup",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11-alpha",
|
|
4
4
|
"description": "Self-host UI SyncUp with a single command",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ui-syncup": "./dist/index.js"
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"commander": "^12.0.0",
|
|
17
17
|
"@inquirer/prompts": "^8.0.0",
|
|
18
|
-
"chalk": "^5.3.0"
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"ora": "^8.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"tsup": "^8.0.0",
|