backdot 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ```bash
12
12
  npm install -g backdot
13
- backdot --init
13
+ backdot init
14
14
  ```
15
15
 
16
16
  This creates `~/.backdot.json` with sensible defaults and walks you through setup. Open the config file and set your repository URL and the files you want backed up:
@@ -26,13 +26,13 @@ This creates `~/.backdot.json` with sensible defaults and walks you through setu
26
26
  Run your first backup:
27
27
 
28
28
  ```bash
29
- backdot --backup
29
+ backdot backup
30
30
  ```
31
31
 
32
32
  or configure the backport process to run automatically (daily at 2am)
33
33
 
34
34
  ```bash
35
- backdot --schedule
35
+ backdot schedule
36
36
  ```
37
37
 
38
38
  ## Configuration
@@ -49,19 +49,25 @@ Prefix a pattern with `!` to exclude matching files:
49
49
  }
50
50
  ```
51
51
 
52
+ ## Encryption
53
+
54
+ To encrypt files before they are pushed to the remote repo, add `"encrypt": true` to your config.
55
+
56
+ On first backup you'll be prompted for a password and offered to save it to `~/.backdot.key` so that future backups do not prompt for a password.
57
+
52
58
  ## Commands
53
59
 
54
- | Command | Description |
55
- | -------------------------------- | ---------------------------------------------- |
56
- | `--init` | Set up backdot for the first time |
57
- | `--backup` | Run a backup now |
58
- | `--restore` | Restore latest backup from the configured repo |
59
- | `--restore <url>` | Restore from a specific repo URL |
60
- | `--restore [url] --commit <sha>` | Restore from a specific backup commit |
61
- | `--history [url]` | Browse and restore a previous backup |
62
- | `--schedule` | Schedule automatic daily backup (Mac-only) |
63
- | `--unschedule` | Unschedule the daily backup |
64
- | `--status` | Show schedule and resolved file list |
60
+ | Command | Description |
61
+ | ------------------------------ | ---------------------------------------------- |
62
+ | `init` | Set up backdot for the first time |
63
+ | `backup` | Run a backup now |
64
+ | `restore` | Restore latest backup from the configured repo |
65
+ | `restore <url>` | Restore from a specific repo URL |
66
+ | `restore [url] --commit <sha>` | Restore from a specific backup commit |
67
+ | `history [url]` | Browse and restore a previous backup |
68
+ | `schedule` | Schedule automatic daily backup (Mac-only) |
69
+ | `unschedule` | Unschedule the daily backup |
70
+ | `status` | Show schedule and resolved file list |
65
71
 
66
72
  ## Development
67
73
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { parseArgs } from "node:util";
4
+ import cac from "cac";
5
5
  import chalk from "chalk";
6
6
  import { CONFIG_PATH } from "./config.js";
7
7
  import { backup } from "./commands/backup.js";
@@ -23,92 +23,48 @@ function getVersion() {
23
23
  return "unknown";
24
24
  }
25
25
  }
26
- function printHelp() {
27
- console.log();
28
- console.log(" Usage: backdot <command>");
29
- console.log();
30
- console.log(" Commands:");
31
- console.log();
32
- console.log(" --init Set up backdot for the first time");
33
- console.log(" --backup Run a backup now");
34
- console.log(" --restore [url] Restore files from the backup repo");
35
- console.log(" --restore [url] --commit <sha> Restore from a specific backup commit");
36
- console.log(" --restore [url] --yes (-y) Accept defaults without prompting");
37
- console.log(" --history [url] Browse and restore a previous backup");
38
- console.log(" --schedule Install daily backup schedule (macOS launchd)");
39
- console.log(" --unschedule Remove the daily backup schedule");
40
- console.log(" --status Show schedule and resolved files");
41
- console.log(" --version Show version");
42
- console.log();
43
- }
44
- async function main() {
45
- let values;
46
- let positionals;
47
- try {
48
- ({ values, positionals } = parseArgs({
49
- args: process.argv.slice(2),
50
- options: {
51
- init: { type: "boolean" },
52
- backup: { type: "boolean" },
53
- restore: { type: "boolean" },
54
- history: { type: "boolean" },
55
- schedule: { type: "boolean" },
56
- unschedule: { type: "boolean" },
57
- status: { type: "boolean" },
58
- version: { type: "boolean" },
59
- help: { type: "boolean" },
60
- commit: { type: "string" },
61
- yes: { type: "boolean", short: "y" },
62
- },
63
- allowPositionals: true,
64
- strict: true,
65
- }));
66
- }
67
- catch (err) {
68
- console.error(`\n Error: ${errorMessage(err)}\n`);
69
- printHelp();
70
- process.exit(1);
26
+ const cli = cac("backdot");
27
+ cli.command("init", "Set up backdot for the first time").action(() => init());
28
+ cli.command("backup", "Run a backup now").action(async () => {
29
+ await backup();
30
+ });
31
+ cli
32
+ .command("restore [url]", "Restore files")
33
+ .option("--commit <sha>", "Restore from a specific backup commit")
34
+ .option("-y, --yes", "Accept defaults without prompting")
35
+ .action(async (url, options) => {
36
+ await restore({ repoUrl: url, commit: options.commit, yes: !!options.yes });
37
+ });
38
+ cli
39
+ .command("history [url]", "List and restore a previous backup")
40
+ .action(async (url) => {
41
+ await history(url);
42
+ });
43
+ cli.command("schedule", "Schedule daily backup").action(() => schedule());
44
+ cli.command("unschedule", "Unschedule the daily backup").action(() => unschedule());
45
+ cli.command("status", "Show the status of the backup").action(async () => {
46
+ await status();
47
+ });
48
+ cli.command("", "").action(() => {
49
+ cli.outputHelp();
50
+ if (!fs.existsSync(CONFIG_PATH)) {
51
+ console.log(` No config found. Run ${chalk.bold("backdot init")} to get started.\n`);
71
52
  }
53
+ });
54
+ // cac auto-adds a --help flag; remove its redundant section from help output
55
+ cli.help((sections) => sections.filter((s) => !s.body?.includes("--help")));
56
+ cli.version(getVersion());
57
+ async function main() {
72
58
  try {
73
- if (values.init) {
74
- init();
75
- }
76
- else if (values.backup) {
77
- await backup();
78
- }
79
- else if (values.schedule) {
80
- schedule();
81
- }
82
- else if (values.unschedule) {
83
- unschedule();
84
- }
85
- else if (values.status) {
86
- await status();
87
- }
88
- else if (values.restore) {
89
- await restore(positionals[0], values.commit, {
90
- yes: !!values.yes,
91
- });
92
- }
93
- else if (values.history) {
94
- await history(positionals[0]);
95
- }
96
- else if (values.version) {
97
- console.log(getVersion());
98
- }
99
- else {
100
- printHelp();
101
- if (!fs.existsSync(CONFIG_PATH)) {
102
- console.log(` No config found. Run ${chalk.bold("backdot --init")} to get started.`);
103
- console.log();
104
- }
105
- }
59
+ cli.parse(process.argv, { run: false });
60
+ await cli.runMatchedCommand();
106
61
  }
107
62
  catch (err) {
108
63
  const msg = errorMessage(err);
109
64
  logger.error(msg);
110
65
  console.error(`\n Error: ${msg}\n`);
111
- if (!process.stdout.isTTY) {
66
+ const isRunningInBackground = !process.stdout.isTTY;
67
+ if (isRunningInBackground) {
112
68
  sendNotification("Backdot", `Backup failed: ${msg}`);
113
69
  }
114
70
  process.exit(1);
@@ -1,17 +1,57 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import ora from "ora";
2
4
  import { loadConfig, CONFIG_PATH } from "../config.js";
3
5
  import { resolveFiles } from "../resolveFiles.js";
4
- import { cleanStaging, copyToStaging, writeRepoReadme } from "../staging.js";
6
+ import { cleanStaging, copyToStaging, writeRepoReadme, machineDir } from "../staging.js";
5
7
  import { gitPull, gitCommitAndPush } from "../git.js";
6
8
  import { logger } from "../log.js";
7
9
  import { pluralize } from "../utils.js";
10
+ import { checkRepoVisibility } from "../repoVisibility.js";
11
+ import { decrypt, deriveKey } from "../crypto/encryption.js";
12
+ import { resolvePassword, offerToSaveKeyFile, confirmPassword, ENC_SUFFIX, } from "../crypto/password.js";
13
+ function findEncryptedFile(dir) {
14
+ if (!fs.existsSync(dir)) {
15
+ return null;
16
+ }
17
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
18
+ const fullPath = path.join(dir, entry.name);
19
+ if (entry.isDirectory()) {
20
+ const encryptedFile = findEncryptedFile(fullPath);
21
+ if (encryptedFile) {
22
+ return encryptedFile;
23
+ }
24
+ }
25
+ else if (entry.name.endsWith(ENC_SUFFIX)) {
26
+ return fullPath;
27
+ }
28
+ }
29
+ return null;
30
+ }
8
31
  export async function backup() {
9
32
  logger.info("Starting backup");
10
33
  const config = loadConfig();
11
34
  logger.info(`Repository: ${config.repository}`);
12
35
  logger.info(`Machine: ${config.machine}`);
13
- const spinner = ora("Resolving files").start();
36
+ let password;
37
+ let derivedKey;
38
+ let passwordWasInteractive = false;
39
+ if (config.encrypt) {
40
+ const result = await resolvePassword();
41
+ password = result.password;
42
+ passwordWasInteractive = result.interactive;
43
+ derivedKey = deriveKey(password);
44
+ }
45
+ const spinner = ora("Checking repository visibility").start();
14
46
  try {
47
+ const visibility = await checkRepoVisibility(config.repository);
48
+ if (visibility === "public") {
49
+ spinner.fail("Backup refused — repository is public");
50
+ throw new Error(`Repository "${config.repository}" is publicly accessible.\n` +
51
+ "Backing up to a public repo would expose sensitive files.\n" +
52
+ "Make the repository private, then try again.");
53
+ }
54
+ spinner.text = "Resolving files";
15
55
  const userFiles = resolveFiles(config);
16
56
  logger.info(`Resolved ${pluralize(userFiles.length, "file")}`);
17
57
  if (userFiles.length === 0) {
@@ -21,10 +61,29 @@ export async function backup() {
21
61
  const files = [...userFiles, CONFIG_PATH];
22
62
  spinner.text = "Syncing with remote";
23
63
  await gitPull(config.repository);
64
+ if (derivedKey) {
65
+ const existingEncryptedFile = findEncryptedFile(machineDir(config.machine));
66
+ if (existingEncryptedFile) {
67
+ spinner.text = "Verifying encryption password";
68
+ const encryptedContent = fs.readFileSync(existingEncryptedFile);
69
+ try {
70
+ decrypt(encryptedContent, derivedKey);
71
+ }
72
+ catch {
73
+ spinner.fail("Backup failed");
74
+ throw new Error("Password does not match the existing backup.");
75
+ }
76
+ }
77
+ else if (passwordWasInteractive) {
78
+ spinner.stop();
79
+ await confirmPassword(password);
80
+ spinner.start();
81
+ }
82
+ }
24
83
  spinner.text = `Copying ${pluralize(files.length, "file")} to staging`;
25
84
  cleanStaging(config.machine);
26
- copyToStaging(files, config.machine);
27
- writeRepoReadme(config.repository);
85
+ copyToStaging(files, config.machine, derivedKey);
86
+ writeRepoReadme(config.repository, config.encrypt);
28
87
  spinner.text = "Pushing to remote";
29
88
  const result = await gitCommitAndPush();
30
89
  const successMsg = result?.commitUrl
@@ -37,5 +96,8 @@ export async function backup() {
37
96
  spinner.fail("Backup failed");
38
97
  throw err;
39
98
  }
99
+ if (password) {
100
+ await offerToSaveKeyFile(password);
101
+ }
40
102
  logger.info("Backup complete");
41
103
  }
@@ -22,14 +22,17 @@ export async function history(repoUrl) {
22
22
  console.log("\n No backup history found.\n");
23
23
  return;
24
24
  }
25
- const selected = await select({
25
+ const selectedCommitHash = await select({
26
26
  message: "Select a backup to restore from:",
27
27
  loop: false,
28
- choices: commits.map((c) => ({
29
- name: `${c.hash.slice(0, 7)} ${c.date.split("T")[0]} ${c.message}`,
30
- value: c.hash,
31
- })),
28
+ choices: commits.map((commit) => {
29
+ const dateOnly = commit.date.split("T")[0];
30
+ return {
31
+ name: `${commit.hash.slice(0, 7)} ${dateOnly} ${commit.message}`,
32
+ value: commit.hash,
33
+ };
34
+ }),
32
35
  });
33
36
  console.log();
34
- await restore(repoUrl, selected);
37
+ await restore({ repoUrl, commit: selectedCommitHash });
35
38
  }
@@ -17,7 +17,7 @@ function getMachineName() {
17
17
  }
18
18
  return os.hostname().replace(/\.(local|localdomain)$/, "");
19
19
  }
20
- const TEMPLATE = {
20
+ const DEFAULT_CONFIG = {
21
21
  repository: "git@github.com:USERNAME/backdot-backup.git",
22
22
  machine: getMachineName(),
23
23
  paths: ["~/.zshrc", "~/.gitconfig"],
@@ -42,16 +42,17 @@ export function init() {
42
42
  console.log(` ${chalk.yellow(`${CONFIG_PATH} already exists — skipping creation.`)}`);
43
43
  }
44
44
  else {
45
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(TEMPLATE, null, 2) + "\n");
45
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
46
46
  console.log(` Created ${CONFIG_PATH} with defaults.`);
47
47
  }
48
48
  console.log(" Open it and set your repository URL and files to back up.");
49
+ console.log(` To encrypt backups, add ${chalk.bold('"encrypt": true')} to the config.`);
49
50
  console.log();
50
51
  // Step 3
51
52
  console.log(chalk.bold(" Step 3 — Run your first backup"));
52
53
  console.log();
53
- console.log(` ${chalk.bold("backdot --backup")} Run a one-time backup`);
54
- console.log(` ${chalk.bold("backdot --schedule")} Schedule daily backups (macOS)`);
55
- console.log(` ${chalk.bold("backdot --status")} Check which files will be backed up`);
54
+ console.log(` ${chalk.bold("backdot backup")} Run a one-time backup`);
55
+ console.log(` ${chalk.bold("backdot schedule")} Schedule daily backups (macOS)`);
56
+ console.log(` ${chalk.bold("backdot status")} Check which files will be backed up`);
56
57
  console.log();
57
58
  }
@@ -1,5 +1,5 @@
1
- interface RestoreOptions {
1
+ export declare function restore({ repoUrl, commit, yes, }?: {
2
+ repoUrl?: string;
3
+ commit?: string;
2
4
  yes?: boolean;
3
- }
4
- export declare function restore(repoUrl?: string, commit?: string, options?: RestoreOptions): Promise<void>;
5
- export {};
5
+ }): Promise<void>;
@@ -8,14 +8,16 @@ import { gitPull } from "../git.js";
8
8
  import { STAGING_DIR, machineDir } from "../staging.js";
9
9
  import { logger } from "../log.js";
10
10
  import { pluralize } from "../utils.js";
11
+ import { decrypt, deriveKey } from "../crypto/encryption.js";
12
+ import { resolvePassword, offerToSaveKeyFile, ENC_SUFFIX } from "../crypto/password.js";
11
13
  const HOME = os.homedir();
12
- function walkDir(dir) {
14
+ function listFilesRecursively(dir) {
13
15
  return fs
14
16
  .readdirSync(dir, { withFileTypes: true })
15
17
  .filter((entry) => entry.name !== ".git")
16
18
  .flatMap((entry) => {
17
- const full = path.join(dir, entry.name);
18
- return entry.isDirectory() ? walkDir(full) : [full];
19
+ const fullPath = path.join(dir, entry.name);
20
+ return entry.isDirectory() ? listFilesRecursively(fullPath) : [fullPath];
19
21
  });
20
22
  }
21
23
  function listMachines() {
@@ -24,8 +26,8 @@ function listMachines() {
24
26
  }
25
27
  return fs
26
28
  .readdirSync(STAGING_DIR, { withFileTypes: true })
27
- .filter((e) => e.isDirectory() && e.name !== ".git")
28
- .map((e) => e.name);
29
+ .filter((entry) => entry.isDirectory() && entry.name !== ".git")
30
+ .map((entry) => entry.name);
29
31
  }
30
32
  async function resolveRepoAndMachine(repoUrl, commit) {
31
33
  if (!repoUrl) {
@@ -58,7 +60,7 @@ async function resolveRepoAndMachine(repoUrl, commit) {
58
60
  }
59
61
  return { repository: repoUrl, machine };
60
62
  }
61
- export async function restore(repoUrl, commit, options = {}) {
63
+ export async function restore({ repoUrl, commit, yes, } = {}) {
62
64
  logger.info("Starting restore");
63
65
  const { repository, machine } = await resolveRepoAndMachine(repoUrl, commit);
64
66
  const spinner = ora("Fetching latest backup").start();
@@ -85,61 +87,80 @@ export async function restore(repoUrl, commit, options = {}) {
85
87
  }
86
88
  return;
87
89
  }
88
- const stagedFiles = walkDir(baseDir);
90
+ const stagedFiles = listFilesRecursively(baseDir);
89
91
  logger.info(`Found ${pluralize(stagedFiles.length, "file")} in backup repository`);
90
92
  if (stagedFiles.length === 0) {
91
93
  spinner.stop();
92
94
  console.log("No files found in backup repository.");
93
95
  return;
94
96
  }
95
- const fileMappings = stagedFiles.map((src) => {
96
- const rel = path.relative(baseDir, src);
97
- return { src, dest: path.join(HOME, rel), rel };
97
+ const fileMappings = stagedFiles.map((stagedFilePath) => {
98
+ let relativePath = path.relative(baseDir, stagedFilePath);
99
+ if (relativePath.endsWith(ENC_SUFFIX)) {
100
+ relativePath = relativePath.slice(0, -ENC_SUFFIX.length);
101
+ }
102
+ return { src: stagedFilePath, dest: path.join(HOME, relativePath), rel: relativePath };
98
103
  });
99
- const existing = fileMappings.filter((f) => fs.existsSync(f.dest));
100
- const fresh = fileMappings.filter((f) => !fs.existsSync(f.dest));
101
- logger.info(`${fresh.length} new, ${existing.length} already exist`);
104
+ const filesAlreadyOnDisk = fileMappings.filter((file) => fs.existsSync(file.dest));
105
+ const newFiles = fileMappings.filter((file) => !fs.existsSync(file.dest));
106
+ logger.info(`${newFiles.length} new, ${filesAlreadyOnDisk.length} already exist`);
102
107
  spinner.stop();
103
108
  console.log();
104
- let toRestore;
105
- if (options.yes) {
106
- toRestore = fresh;
107
- if (existing.length > 0) {
108
- console.log(` Skipped ${pluralize(existing.length, "existing file")}. Run without --yes to select them.`);
109
+ let filesToRestore;
110
+ if (yes) {
111
+ filesToRestore = newFiles;
112
+ if (filesAlreadyOnDisk.length > 0) {
113
+ console.log(` Skipped ${pluralize(filesAlreadyOnDisk.length, "existing file")}. Run without --yes to select them.`);
109
114
  console.log();
110
115
  }
111
116
  }
112
117
  else {
113
118
  const choices = [];
114
- if (fresh.length > 0) {
115
- choices.push(new Separator(`── New files (${fresh.length}) ──`));
116
- for (const f of fresh) {
117
- choices.push({ name: f.rel, value: f, checked: true });
119
+ if (newFiles.length > 0) {
120
+ choices.push(new Separator(`── New files (${newFiles.length}) ──`));
121
+ for (const file of newFiles) {
122
+ choices.push({ name: file.rel, value: file, checked: true });
118
123
  }
119
124
  }
120
- if (existing.length > 0) {
121
- choices.push(new Separator(`── Existing files — will overwrite (${existing.length}) ──`));
122
- for (const f of existing) {
123
- choices.push({ name: f.rel, value: f, checked: false });
125
+ if (filesAlreadyOnDisk.length > 0) {
126
+ choices.push(new Separator(`── Existing files — will overwrite (${filesAlreadyOnDisk.length}) ──`));
127
+ for (const file of filesAlreadyOnDisk) {
128
+ choices.push({ name: file.rel, value: file, checked: false });
124
129
  }
125
130
  }
126
- toRestore = await checkbox({
131
+ filesToRestore = await checkbox({
127
132
  message: "Select files to restore:",
128
133
  loop: false,
129
134
  choices,
130
135
  });
131
136
  console.log();
132
137
  }
133
- if (toRestore.length === 0) {
138
+ if (filesToRestore.length === 0) {
134
139
  console.log("No files selected for restore.");
135
140
  return;
136
141
  }
137
- const copySpinner = ora("Restoring files").start();
138
- for (const { src, dest } of toRestore) {
142
+ let password;
143
+ const hasEncryptedFiles = filesToRestore.some(({ src }) => src.endsWith(ENC_SUFFIX));
144
+ if (hasEncryptedFiles) {
145
+ const result = await resolvePassword();
146
+ password = result.password;
147
+ }
148
+ const derivedKey = password ? deriveKey(password) : undefined;
149
+ const restoreSpinner = ora("Restoring files").start();
150
+ for (const { src, dest } of filesToRestore) {
139
151
  fs.mkdirSync(path.dirname(dest), { recursive: true });
140
- fs.copyFileSync(src, dest);
152
+ if (derivedKey && src.endsWith(ENC_SUFFIX)) {
153
+ const content = fs.readFileSync(src);
154
+ fs.writeFileSync(dest, decrypt(content, derivedKey));
155
+ }
156
+ else {
157
+ fs.copyFileSync(src, dest);
158
+ }
141
159
  }
142
- copySpinner.succeed(`Restored ${pluralize(toRestore.length, "file")}`);
160
+ restoreSpinner.succeed(`Restored ${pluralize(filesToRestore.length, "file")}`);
143
161
  console.log();
144
- logger.info(`Restored ${pluralize(toRestore.length, "file")}`);
162
+ if (password) {
163
+ await offerToSaveKeyFile(password);
164
+ }
165
+ logger.info(`Restored ${pluralize(filesToRestore.length, "file")}`);
145
166
  }
@@ -1,4 +1,8 @@
1
+ import fs from "node:fs";
2
+ import chalk from "chalk";
1
3
  import { setupLaunchd, uninstallLaunchd } from "../launchd.js";
4
+ import { loadConfig } from "../config.js";
5
+ import { KEY_FILE_PATH } from "../crypto/password.js";
2
6
  function requireMacOS() {
3
7
  if (process.platform !== "darwin") {
4
8
  throw new Error("Scheduling is only supported on macOS (launchd). Use cron or systemd on Linux.");
@@ -7,6 +11,16 @@ function requireMacOS() {
7
11
  export function schedule() {
8
12
  requireMacOS();
9
13
  setupLaunchd();
14
+ try {
15
+ const config = loadConfig();
16
+ if (config.encrypt && !fs.existsSync(KEY_FILE_PATH)) {
17
+ console.log(chalk.yellow(` Encryption is enabled. Run ${chalk.bold("backdot backup")} once to create ~/.backdot.key,\n` +
18
+ ` or set ${chalk.bold("BACKDOT_PASSWORD")} in your environment.\n`));
19
+ }
20
+ }
21
+ catch {
22
+ // Config may not exist yet; ignore
23
+ }
10
24
  }
11
25
  export function unschedule() {
12
26
  requireMacOS();
@@ -6,17 +6,47 @@ import { resolveFiles } from "../resolveFiles.js";
6
6
  import { compareFiles } from "../staging.js";
7
7
  import { isScheduled } from "../launchd.js";
8
8
  import { pluralize } from "../utils.js";
9
- function tildePath(filePath) {
9
+ import { checkRepoVisibility } from "../repoVisibility.js";
10
+ import { deriveKey } from "../crypto/encryption.js";
11
+ import { resolvePassword } from "../crypto/password.js";
12
+ function abbreviateHomePath(filePath) {
10
13
  const home = os.homedir();
11
14
  return filePath.startsWith(home) ? "~" + filePath.slice(home.length) : filePath;
12
15
  }
16
+ function formatVisibility(visibility) {
17
+ switch (visibility) {
18
+ case "public":
19
+ return chalk.red.bold("public (backup disabled)");
20
+ case "private":
21
+ return chalk.green("private");
22
+ case "unknown":
23
+ return chalk.yellow("unknown (could not verify)");
24
+ }
25
+ }
13
26
  export async function status() {
14
27
  const scheduled = isScheduled();
15
28
  console.log();
16
- console.log(` Schedule: ${scheduled ? "active (daily at 02:00)" : `not active (run ${chalk.bold("backdot --schedule")} to enable)`}`);
29
+ console.log(` Schedule: ${scheduled ? "active (daily at 02:00)" : `not active (run ${chalk.bold("backdot schedule")} to enable)`}`);
17
30
  const config = loadConfig();
18
- console.log(` Repo: ${config.repository}`);
19
- console.log(` Machine: ${config.machine}`);
31
+ console.log(` Repo: ${config.repository}`);
32
+ console.log(` Machine: ${config.machine}`);
33
+ if (config.encrypt) {
34
+ console.log(` Encryption: ${chalk.green("enabled")}`);
35
+ }
36
+ const visibilitySpinner = ora("Checking repository visibility").start();
37
+ const visibility = await checkRepoVisibility(config.repository);
38
+ visibilitySpinner.stop();
39
+ if (visibility !== "private") {
40
+ console.log(` Visibility: ${formatVisibility(visibility)}`);
41
+ }
42
+ if (visibility === "public") {
43
+ console.log();
44
+ console.log(chalk.red(" ⚠ Repository is public. Backup is disabled to prevent leaking sensitive files.\n" +
45
+ " Make the repository private, then try again."));
46
+ console.log();
47
+ return;
48
+ }
49
+ const derivedKey = config.encrypt ? deriveKey((await resolvePassword()).password) : undefined;
20
50
  console.log();
21
51
  const spinner = ora("Resolving files").start();
22
52
  try {
@@ -28,7 +58,12 @@ export async function status() {
28
58
  }
29
59
  const files = [...userFiles, CONFIG_PATH];
30
60
  spinner.text = "Comparing with remote backup";
31
- const { backedUp, modified, notBackedUp, error } = await compareFiles(files, config.machine, config.repository);
61
+ const { backedUp, modified, notBackedUp, error } = await compareFiles({
62
+ files,
63
+ machine: config.machine,
64
+ repository: config.repository,
65
+ derivedKey,
66
+ });
32
67
  spinner.stop();
33
68
  if (error) {
34
69
  console.log(chalk.yellow(` Could not fetch status: ${error}`));
@@ -40,26 +75,26 @@ export async function status() {
40
75
  else {
41
76
  if (modified.length > 0) {
42
77
  console.log(chalk.yellow(` Modified since last backup (${modified.length}):`));
43
- for (const f of modified) {
44
- console.log(` ${tildePath(f)}`);
78
+ for (const filePath of modified) {
79
+ console.log(` ${abbreviateHomePath(filePath)}`);
45
80
  }
46
81
  console.log();
47
82
  }
48
83
  if (notBackedUp.length > 0) {
49
84
  console.log(chalk.red(` Not yet backed up (${notBackedUp.length}):`));
50
- for (const f of notBackedUp) {
51
- console.log(` ${tildePath(f)}`);
85
+ for (const filePath of notBackedUp) {
86
+ console.log(` ${abbreviateHomePath(filePath)}`);
52
87
  }
53
88
  console.log();
54
89
  }
55
90
  if (backedUp.length > 0) {
56
91
  console.log(chalk.green(` Backed up (${backedUp.length}):`));
57
- for (const f of backedUp) {
58
- console.log(` ${tildePath(f)}`);
92
+ for (const filePath of backedUp) {
93
+ console.log(` ${abbreviateHomePath(filePath)}`);
59
94
  }
60
95
  console.log();
61
96
  }
62
- console.log(` Run ${chalk.bold("backdot --backup")} to back up all changes.`);
97
+ console.log(` Run ${chalk.bold("backdot backup")} to back up all changes.`);
63
98
  }
64
99
  }
65
100
  catch (err) {