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.
Files changed (3) hide show
  1. package/README.md +94 -0
  2. package/dist/index.js +391 -25
  3. 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
- info: (msg) => console.log(import_chalk.default.blue("\u2139"), msg),
38
- success: (msg) => console.log(import_chalk.default.green("\u2714"), msg),
39
- warn: (msg) => console.log(import_chalk.default.yellow("\u26A0"), msg),
40
- error: (msg) => console.error(import_chalk.default.red("\u2716"), msg),
41
- step: (n, total, msg) => console.log(import_chalk.default.dim(`[${n}/${total}]`), msg),
42
- header: (msg) => console.log("\n" + import_chalk.default.bold.blue(msg) + "\n")
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.header("UI SyncUp \u2014 Setup Wizard");
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
- ui.step(1, 2, "Pulling latest image...");
263
- const pull = runCompose(composeFile, ["pull"]);
264
- if (!pull.success) {
265
- ui.error("docker compose pull failed");
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
- ui.step(2, 2, "Restarting stack (migrations run automatically on container start)...");
269
- const up = runCompose(composeFile, ["up", "-d", "--remove-orphans"]);
270
- if (!up.success) {
271
- ui.error("docker compose up failed \u2014 check logs with: docker compose logs app");
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
- ui.success("Upgrade complete. Migrations applied automatically via the app entrypoint.");
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 import_node_child_process3 = require("child_process");
279
- var import_node_fs3 = require("fs");
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, import_node_fs3.existsSync)(".env")) {
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, import_node_fs3.existsSync)(".env") ? parseEnv(".env") : {};
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, import_node_child_process3.execSync)(`curl -sf "${appUrl}/api/health"`, {
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, import_node_child_process3.execSync)("df -k . | awk 'NR==2{print $4}'", {
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("0.2.4");
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.6",
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",