backdot 1.4.0 → 1.5.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
@@ -4,7 +4,7 @@
4
4
 
5
5
  <h1 align="center">backdot</h1>
6
6
 
7
- <p align="center">Automated backup of important files (configs, dotfiles, gitignored files) to your own private Git repo.</p>
7
+ <p align="center">Automated backup of important files (configs, dotfiles) to your own private Git repo.</p>
8
8
 
9
9
  ## Getting started
10
10
 
@@ -19,7 +19,6 @@ This creates `~/.backdot.json` with sensible defaults and walks you through setu
19
19
  {
20
20
  "repository": "git@github.com:USERNAME/backdot-backup.git",
21
21
  "machine": "my-work-laptop",
22
- "gitignored": ["~/my-project"],
23
22
  "paths": ["~/.zshrc", "~/.oh-my-zsh/custom/*.zsh", "~/.ssh/config", "~/.npmrc"]
24
23
  }
25
24
  ```
@@ -38,10 +37,17 @@ backdot --schedule
38
37
 
39
38
  ## Configuration
40
39
 
41
- | Key | Description |
42
- | ------------ | ------------------------------------------------------ |
43
- | `gitignored` | Directories to scan for gitignored files |
44
- | `paths` | Glob patterns matching individual files or directories |
40
+ | Key | Description |
41
+ | ------- | ------------------------------------------------------ |
42
+ | `paths` | Glob patterns matching individual files or directories |
43
+
44
+ Prefix a pattern with `!` to exclude matching files:
45
+
46
+ ```json
47
+ {
48
+ "paths": ["~/.config/ghostty/**", "!~/.config/ghostty/crash-reports/**"]
49
+ }
50
+ ```
45
51
 
46
52
  ## Commands
47
53
 
package/dist/cli.js CHANGED
@@ -1,16 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
- import os from "node:os";
4
3
  import path from "node:path";
5
4
  import chalk from "chalk";
6
- import ora from "ora";
7
- import { loadConfig, CONFIG_PATH } from "./config.js";
8
- import { resolveFiles } from "./resolve.js";
9
- import { cleanStaging, copyToStaging, writeRepoReadme, compareFiles } from "./staging.js";
10
- import { gitPull, gitCommitAndPush } from "./git.js";
11
- import { restore } from "./restore.js";
12
- import { init } from "./init.js";
13
- import { setupLaunchd, uninstallLaunchd, isScheduled } from "./plist.js";
5
+ import { CONFIG_PATH } from "./config.js";
6
+ import { backup } from "./commands/backup.js";
7
+ import { status } from "./commands/status.js";
8
+ import { schedule, unschedule } from "./commands/schedule.js";
9
+ import { restore } from "./commands/restore.js";
10
+ import { init } from "./commands/init.js";
14
11
  import { logger } from "./log.js";
15
12
  import { sendNotification } from "./notify.js";
16
13
  function getVersion() {
@@ -23,104 +20,6 @@ function getVersion() {
23
20
  return "unknown";
24
21
  }
25
22
  }
26
- async function backup() {
27
- logger.info("Starting backup");
28
- const config = loadConfig();
29
- logger.info(`Repository: ${config.repository}`);
30
- logger.info(`Machine: ${config.machine}`);
31
- const spinner = ora("Resolving files").start();
32
- try {
33
- const userFiles = resolveFiles(config);
34
- logger.info(`Resolved ${userFiles.length} file(s)`);
35
- if (userFiles.length === 0) {
36
- spinner.info("No files resolved, nothing to back up");
37
- return;
38
- }
39
- const files = [...userFiles, CONFIG_PATH];
40
- spinner.text = "Syncing with remote";
41
- await gitPull(config.repository);
42
- spinner.text = `Copying ${files.length} file(s) to staging`;
43
- cleanStaging(config.machine);
44
- copyToStaging(files, config.machine);
45
- writeRepoReadme(config.repository);
46
- spinner.text = "Pushing to remote";
47
- const result = await gitCommitAndPush();
48
- const successMsg = result?.commitUrl
49
- ? `Backup complete → ${result.commitUrl}`
50
- : "Backup complete";
51
- spinner.succeed(successMsg);
52
- console.log();
53
- }
54
- catch (err) {
55
- spinner.fail("Backup failed");
56
- throw err;
57
- }
58
- logger.info("Backup complete");
59
- }
60
- function tildePath(filePath) {
61
- const home = os.homedir();
62
- return filePath.startsWith(home) ? "~" + filePath.slice(home.length) : filePath;
63
- }
64
- async function status() {
65
- const scheduled = isScheduled();
66
- console.log();
67
- console.log(` Schedule: ${scheduled ? "active (daily at 02:00)" : "not active"}`);
68
- try {
69
- const config = loadConfig();
70
- console.log(` Repo: ${config.repository}`);
71
- console.log(` Machine: ${config.machine}`);
72
- console.log();
73
- const spinner = ora("Resolving files").start();
74
- const userFiles = resolveFiles(config);
75
- if (userFiles.length === 0) {
76
- spinner.warn("No files resolved. Check your ~/.backdot.json entries.");
77
- console.log();
78
- return;
79
- }
80
- const files = [...userFiles, CONFIG_PATH];
81
- spinner.text = "Comparing with remote backup";
82
- const { backedUp, modified, notBackedUp } = await compareFiles(files, config.machine);
83
- spinner.stop();
84
- if (modified.length === 0 && notBackedUp.length === 0) {
85
- console.log(chalk.green(` All ${files.length} file(s) are backed up ✓`));
86
- }
87
- else {
88
- if (modified.length > 0) {
89
- console.log(chalk.yellow(` Modified since last backup (${modified.length}):`));
90
- for (const f of modified) {
91
- console.log(` ${tildePath(f)}`);
92
- }
93
- console.log();
94
- }
95
- if (notBackedUp.length > 0) {
96
- console.log(chalk.red(` Not yet backed up (${notBackedUp.length}):`));
97
- for (const f of notBackedUp) {
98
- console.log(` ${tildePath(f)}`);
99
- }
100
- console.log();
101
- }
102
- if (backedUp.length > 0) {
103
- console.log(chalk.green(` Backed up (${backedUp.length}):`));
104
- for (const f of backedUp) {
105
- console.log(` ${tildePath(f)}`);
106
- }
107
- console.log();
108
- }
109
- console.log(` Run ${chalk.bold("backdot --backup")} to back up all changes.`);
110
- }
111
- }
112
- catch (err) {
113
- const msg = err instanceof Error ? err.message : String(err);
114
- console.error(`\n Config error: ${msg}`);
115
- process.exit(1);
116
- }
117
- console.log();
118
- }
119
- function requireMacOS() {
120
- if (process.platform !== "darwin") {
121
- throw new Error("Scheduling is only supported on macOS (launchd). Use cron or systemd on Linux.");
122
- }
123
- }
124
23
  async function main() {
125
24
  const args = process.argv.slice(2);
126
25
  const command = args[0];
@@ -132,18 +31,20 @@ async function main() {
132
31
  await backup();
133
32
  }
134
33
  else if (command === "--schedule") {
135
- requireMacOS();
136
- setupLaunchd();
34
+ schedule();
137
35
  }
138
36
  else if (command === "--unschedule") {
139
- requireMacOS();
140
- uninstallLaunchd();
37
+ unschedule();
141
38
  }
142
39
  else if (command === "--status") {
143
40
  await status();
144
41
  }
145
42
  else if (command === "--restore") {
146
- await restore(args[1]);
43
+ const url = args[1];
44
+ if (url?.startsWith("--")) {
45
+ throw new Error(`Invalid repository URL: "${url}". Did you mean to pass a Git URL?`);
46
+ }
47
+ await restore(url);
147
48
  }
148
49
  else if (command === "--version") {
149
50
  console.log(getVersion());
@@ -0,0 +1 @@
1
+ export declare function backup(): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import ora from "ora";
2
+ import { loadConfig, CONFIG_PATH } from "../config.js";
3
+ import { resolveFiles } from "../resolve.js";
4
+ import { cleanStaging, copyToStaging, writeRepoReadme } from "../staging.js";
5
+ import { gitPull, gitCommitAndPush } from "../git.js";
6
+ import { logger } from "../log.js";
7
+ export async function backup() {
8
+ logger.info("Starting backup");
9
+ const config = loadConfig();
10
+ logger.info(`Repository: ${config.repository}`);
11
+ logger.info(`Machine: ${config.machine}`);
12
+ const spinner = ora("Resolving files").start();
13
+ try {
14
+ const userFiles = resolveFiles(config);
15
+ logger.info(`Resolved ${userFiles.length} file(s)`);
16
+ if (userFiles.length === 0) {
17
+ spinner.info("No files resolved, nothing to back up");
18
+ return;
19
+ }
20
+ const files = [...userFiles, CONFIG_PATH];
21
+ spinner.text = "Syncing with remote";
22
+ await gitPull(config.repository);
23
+ spinner.text = `Copying ${files.length} file(s) to staging`;
24
+ cleanStaging(config.machine);
25
+ copyToStaging(files, config.machine);
26
+ writeRepoReadme(config.repository);
27
+ spinner.text = "Pushing to remote";
28
+ const result = await gitCommitAndPush();
29
+ const successMsg = result?.commitUrl
30
+ ? `Backup complete → ${result.commitUrl}`
31
+ : "Backup complete";
32
+ spinner.succeed(successMsg);
33
+ console.log();
34
+ }
35
+ catch (err) {
36
+ spinner.fail("Backup failed");
37
+ throw err;
38
+ }
39
+ logger.info("Backup complete");
40
+ }
@@ -1,11 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import chalk from "chalk";
4
- import { CONFIG_PATH } from "./config.js";
4
+ import { CONFIG_PATH } from "../config.js";
5
5
  const TEMPLATE = {
6
6
  repository: "git@github.com:USERNAME/backdot-backup.git",
7
7
  machine: os.hostname(),
8
- gitignored: [],
9
8
  paths: ["~/.zshrc", "~/.gitconfig"],
10
9
  };
11
10
  export function init() {
@@ -3,10 +3,10 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import ora from "ora";
5
5
  import { checkbox, select } from "@inquirer/prompts";
6
- import { loadConfig } from "./config.js";
7
- import { gitPull } from "./git.js";
8
- import { STAGING_DIR, machineDir } from "./staging.js";
9
- import { logger } from "./log.js";
6
+ import { loadConfig } from "../config.js";
7
+ import { gitPull } from "../git.js";
8
+ import { STAGING_DIR, machineDir } from "../staging.js";
9
+ import { logger } from "../log.js";
10
10
  const HOME = os.homedir();
11
11
  function walkDir(dir) {
12
12
  return fs
@@ -0,0 +1,2 @@
1
+ export declare function schedule(): void;
2
+ export declare function unschedule(): void;
@@ -0,0 +1,14 @@
1
+ import { setupLaunchd, uninstallLaunchd } from "../plist.js";
2
+ function requireMacOS() {
3
+ if (process.platform !== "darwin") {
4
+ throw new Error("Scheduling is only supported on macOS (launchd). Use cron or systemd on Linux.");
5
+ }
6
+ }
7
+ export function schedule() {
8
+ requireMacOS();
9
+ setupLaunchd();
10
+ }
11
+ export function unschedule() {
12
+ requireMacOS();
13
+ uninstallLaunchd();
14
+ }
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import os from "node:os";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { loadConfig, CONFIG_PATH } from "../config.js";
5
+ import { resolveFiles } from "../resolve.js";
6
+ import { compareFiles } from "../staging.js";
7
+ import { isScheduled } from "../plist.js";
8
+ function tildePath(filePath) {
9
+ const home = os.homedir();
10
+ return filePath.startsWith(home) ? "~" + filePath.slice(home.length) : filePath;
11
+ }
12
+ export async function status() {
13
+ const scheduled = isScheduled();
14
+ console.log();
15
+ console.log(` Schedule: ${scheduled ? "active (daily at 02:00)" : "not active"}`);
16
+ const config = loadConfig();
17
+ console.log(` Repo: ${config.repository}`);
18
+ console.log(` Machine: ${config.machine}`);
19
+ console.log();
20
+ const spinner = ora("Resolving files").start();
21
+ try {
22
+ const userFiles = resolveFiles(config);
23
+ if (userFiles.length === 0) {
24
+ spinner.warn("No files resolved. Check your ~/.backdot.json entries.");
25
+ console.log();
26
+ return;
27
+ }
28
+ const files = [...userFiles, CONFIG_PATH];
29
+ spinner.text = "Comparing with remote backup";
30
+ const { backedUp, modified, notBackedUp, error } = await compareFiles(files, config.machine);
31
+ spinner.stop();
32
+ if (error) {
33
+ console.log(chalk.yellow(` Could not fetch status: ${error}`));
34
+ return;
35
+ }
36
+ if (modified.length === 0 && notBackedUp.length === 0) {
37
+ console.log(chalk.green(` All ${files.length} file(s) are backed up ✓`));
38
+ }
39
+ else {
40
+ if (modified.length > 0) {
41
+ console.log(chalk.yellow(` Modified since last backup (${modified.length}):`));
42
+ for (const f of modified) {
43
+ console.log(` ${tildePath(f)}`);
44
+ }
45
+ console.log();
46
+ }
47
+ if (notBackedUp.length > 0) {
48
+ console.log(chalk.red(` Not yet backed up (${notBackedUp.length}):`));
49
+ for (const f of notBackedUp) {
50
+ console.log(` ${tildePath(f)}`);
51
+ }
52
+ console.log();
53
+ }
54
+ if (backedUp.length > 0) {
55
+ console.log(chalk.green(` Backed up (${backedUp.length}):`));
56
+ for (const f of backedUp) {
57
+ console.log(` ${tildePath(f)}`);
58
+ }
59
+ console.log();
60
+ }
61
+ console.log(` Run ${chalk.bold("backdot --backup")} to back up all changes.`);
62
+ }
63
+ }
64
+ catch (err) {
65
+ spinner.fail("Status check failed");
66
+ throw err;
67
+ }
68
+ console.log();
69
+ }
package/dist/config.d.ts CHANGED
@@ -4,8 +4,7 @@ export declare function expandTilde(p: string): string;
4
4
  declare const ConfigSchema: z.ZodObject<{
5
5
  repository: z.ZodString;
6
6
  machine: z.ZodString;
7
- gitignored: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>>;
8
- paths: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>>;
7
+ paths: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
9
8
  }, z.core.$strip>;
10
9
  export type Config = z.infer<typeof ConfigSchema>;
11
10
  export declare function loadConfig(): Config;
package/dist/config.js CHANGED
@@ -4,21 +4,20 @@ import os from "node:os";
4
4
  import { z } from "zod";
5
5
  export const CONFIG_PATH = path.join(os.homedir(), ".backdot.json");
6
6
  export function expandTilde(p) {
7
+ if (p.startsWith("!")) {
8
+ return "!" + expandTilde(p.slice(1));
9
+ }
7
10
  if (p.startsWith("~/") || p === "~") {
8
11
  return path.join(os.homedir(), p.slice(1));
9
12
  }
10
13
  return p;
11
14
  }
12
- const pathList = z.array(z.string().min(1).transform(expandTilde)).optional().default([]);
13
- const ConfigSchema = z
14
- .object({
15
+ const ConfigSchema = z.object({
15
16
  repository: z.string().min(1),
16
17
  machine: z.string().min(1),
17
- gitignored: pathList,
18
- paths: pathList,
19
- })
20
- .refine((c) => c.gitignored.length > 0 || c.paths.length > 0, {
21
- message: 'At least one of "gitignored" or "paths" must be a non-empty array',
18
+ paths: z
19
+ .array(z.string().min(1).transform(expandTilde))
20
+ .min(1, '"paths" must be a non-empty array'),
22
21
  });
23
22
  export function loadConfig() {
24
23
  if (!fs.existsSync(CONFIG_PATH)) {
package/dist/git.js CHANGED
@@ -43,11 +43,17 @@ export async function gitCommitAndPush() {
43
43
  logger.info(`Push failed (attempt ${attemptNumber}, ${retriesLeft} retries left), rebasing`);
44
44
  await git.fetch("origin");
45
45
  const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
46
- await git.rebase([`origin/${branch}`]);
46
+ try {
47
+ await git.rebase([`origin/${branch}`]);
48
+ }
49
+ catch {
50
+ await git.rebase(["--abort"]);
51
+ throw new Error("Rebase conflict, aborting retry");
52
+ }
47
53
  },
48
54
  });
49
55
  logger.info(`Committed and pushed: ${message}`);
50
- const sha = await git.revparse(["HEAD"]);
56
+ const sha = (await git.revparse(["HEAD"])).trim();
51
57
  const remoteUrl = (await git.remote(["get-url", "origin"])) ?? "";
52
58
  const commitUrl = getCommitUrl(remoteUrl, sha);
53
59
  return { commitUrl };
package/dist/log.js CHANGED
@@ -1,7 +1,10 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import os from "node:os";
3
4
  import winston from "winston";
4
- const LOG_FILE = path.join(os.homedir(), ".backdot", "backup.log");
5
+ const LOG_DIR = path.join(os.homedir(), ".backdot");
6
+ fs.mkdirSync(LOG_DIR, { recursive: true });
7
+ const LOG_FILE = path.join(LOG_DIR, "backup.log");
5
8
  export const logger = winston.createLogger({
6
9
  level: "info",
7
10
  format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.printf(({ timestamp, level, message }) => `${timestamp} [${level}] ${message}`)),
package/dist/notify.js CHANGED
@@ -1,12 +1,18 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
2
  import { logger } from "./log.js";
3
+ function escapeAppleScript(str) {
4
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5
+ }
3
6
  export function sendNotification(title, message) {
4
7
  if (process.platform !== "darwin")
5
8
  return;
6
- const escaped = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
7
- const titleEscaped = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
9
+ const escaped = escapeAppleScript(message);
10
+ const titleEscaped = escapeAppleScript(title);
8
11
  try {
9
- execSync(`osascript -e 'display notification "${escaped}" with title "${titleEscaped}" subtitle "Scheduled backup failed"'`, { stdio: "pipe" });
12
+ execFileSync("osascript", [
13
+ "-e",
14
+ `display notification "${escaped}" with title "${titleEscaped}" subtitle "Scheduled backup failed"`,
15
+ ], { stdio: "pipe" });
10
16
  }
11
17
  catch (err) {
12
18
  const msg = err instanceof Error ? err.message : String(err);
package/dist/plist.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
@@ -50,7 +50,7 @@ export function isScheduled() {
50
50
  return false;
51
51
  }
52
52
  try {
53
- const output = execSync(`launchctl list ${LABEL}`, { encoding: "utf-8", stdio: "pipe" });
53
+ const output = execFileSync("launchctl", ["list", LABEL], { encoding: "utf-8", stdio: "pipe" });
54
54
  return output.includes(LABEL);
55
55
  }
56
56
  catch {
@@ -65,7 +65,7 @@ export function setupLaunchd() {
65
65
  fs.mkdirSync(dir, { recursive: true });
66
66
  }
67
67
  try {
68
- execSync(`launchctl unload ${PLIST_PATH}`, { stdio: "pipe" });
68
+ execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
69
69
  }
70
70
  catch {
71
71
  // Not loaded, that's fine
@@ -73,7 +73,7 @@ export function setupLaunchd() {
73
73
  fs.writeFileSync(PLIST_PATH, plistContent);
74
74
  logger.info(`Plist written to ${PLIST_PATH}`);
75
75
  try {
76
- execSync(`launchctl load ${PLIST_PATH}`);
76
+ execFileSync("launchctl", ["load", PLIST_PATH], { stdio: "pipe" });
77
77
  spinner.succeed("Daily backup scheduled (02:00)");
78
78
  console.log();
79
79
  logger.info("Launchd job loaded");
@@ -83,13 +83,13 @@ export function setupLaunchd() {
83
83
  spinner.fail(`Failed to load launchd job: ${msg}`);
84
84
  console.log();
85
85
  logger.error(`Failed to load launchd job: ${msg}`);
86
- process.exit(1);
86
+ throw new Error(`Failed to load launchd job: ${msg}`, { cause: err });
87
87
  }
88
88
  }
89
89
  export function uninstallLaunchd() {
90
90
  const spinner = ora("Removing schedule").start();
91
91
  try {
92
- execSync(`launchctl unload ${PLIST_PATH}`, { stdio: "pipe" });
92
+ execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
93
93
  logger.info("Launchd job unloaded");
94
94
  }
95
95
  catch {
package/dist/resolve.js CHANGED
@@ -1,34 +1,14 @@
1
- import { execSync } from "node:child_process";
2
1
  import fs from "node:fs";
3
- import path from "node:path";
4
2
  import fg from "fast-glob";
5
3
  import { logger } from "./log.js";
6
- function resolveGitignored(dirPath) {
7
- if (!fs.existsSync(dirPath)) {
8
- logger.warn(`Directory does not exist, skipping: ${dirPath}`);
4
+ function resolveGlobs(patterns) {
5
+ if (patterns.length === 0)
9
6
  return [];
10
- }
11
- try {
12
- const output = execSync("git ls-files --others --ignored --exclude-standard", {
13
- cwd: dirPath,
14
- encoding: "utf-8",
15
- });
16
- return output
17
- .split("\n")
18
- .filter((line) => line.length > 0)
19
- .map((rel) => path.resolve(dirPath, rel));
20
- }
21
- catch {
22
- logger.warn(`Failed to list gitignored files in: ${dirPath}`);
23
- return [];
24
- }
25
- }
26
- function resolveGlob(pattern) {
27
7
  try {
28
- return fg.sync(pattern, { absolute: true, dot: true });
8
+ return fg.sync(patterns, { absolute: true, dot: true, onlyFiles: true });
29
9
  }
30
10
  catch {
31
- logger.warn(`Glob pattern failed: ${pattern}`);
11
+ logger.warn("Glob pattern resolution failed");
32
12
  return [];
33
13
  }
34
14
  }
@@ -38,12 +18,7 @@ const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
38
18
  * Skips entries that fail resolution and logs warnings.
39
19
  */
40
20
  export function resolveFiles(config) {
41
- const unique = [
42
- ...new Set([
43
- ...config.gitignored.flatMap(resolveGitignored),
44
- ...config.paths.flatMap(resolveGlob),
45
- ]),
46
- ];
21
+ const unique = [...new Set(resolveGlobs(config.paths))];
47
22
  return unique.filter((filePath) => {
48
23
  try {
49
24
  fs.accessSync(filePath, fs.constants.R_OK);
package/dist/staging.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface ComparisonResult {
7
7
  backedUp: string[];
8
8
  modified: string[];
9
9
  notBackedUp: string[];
10
+ error?: string;
10
11
  }
11
12
  export declare function compareFiles(files: string[], machine: string): Promise<ComparisonResult>;
12
13
  export declare function writeRepoReadme(repository: string): void;
package/dist/staging.js CHANGED
@@ -38,25 +38,30 @@ export function copyToStaging(files, machine) {
38
38
  }
39
39
  logger.info(`Copied ${copied} file(s) to staging`);
40
40
  }
41
- const allNotBackedUp = (files) => ({
42
- backedUp: [],
43
- modified: [],
44
- notBackedUp: files,
45
- });
41
+ function failedComparisonResult(err) {
42
+ const errorMessage = err instanceof Error ? err.message : String(err);
43
+ return { backedUp: [], modified: [], notBackedUp: [], error: errorMessage };
44
+ }
46
45
  export async function compareFiles(files, machine) {
47
46
  if (files.length === 0)
48
47
  return { backedUp: [], modified: [], notBackedUp: [] };
49
48
  const gitDir = path.join(STAGING_DIR, ".git");
50
- if (!fs.existsSync(gitDir))
51
- return allNotBackedUp(files);
49
+ if (!fs.existsSync(gitDir)) {
50
+ return failedComparisonResult(new Error("Backup repository not found. Run backdot --backup first."));
51
+ }
52
52
  const git = simpleGit(STAGING_DIR);
53
- await git.fetch("origin");
53
+ try {
54
+ await git.fetch("origin");
55
+ }
56
+ catch (err) {
57
+ return failedComparisonResult(err);
58
+ }
54
59
  let branch;
55
60
  try {
56
61
  branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
57
62
  }
58
- catch {
59
- return allNotBackedUp(files);
63
+ catch (err) {
64
+ return failedComparisonResult(err);
60
65
  }
61
66
  let committedHashes;
62
67
  try {
@@ -70,8 +75,8 @@ export async function compareFiles(files, machine) {
70
75
  .filter((m) => m !== null)
71
76
  .map((m) => [m[2], m[1]]));
72
77
  }
73
- catch {
74
- return allNotBackedUp(files);
78
+ catch (err) {
79
+ return failedComparisonResult(err);
75
80
  }
76
81
  let sourceHashes;
77
82
  try {
@@ -80,8 +85,8 @@ export async function compareFiles(files, machine) {
80
85
  });
81
86
  sourceHashes = hashOutput.trim().split("\n");
82
87
  }
83
- catch {
84
- return allNotBackedUp(files);
88
+ catch (err) {
89
+ return failedComparisonResult(err);
85
90
  }
86
91
  return files.reduce((acc, file, i) => {
87
92
  const repoRelPath = path.relative(STAGING_DIR, getStagedPath(file, machine));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backdot",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Lightweight CLI to backup dotfiles and gitignored files to a Git repo on a daily schedule",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,8 @@
34
34
  "format:check": "prettier --check src/",
35
35
  "test": "vitest run --exclude src/e2e.test.ts",
36
36
  "test:e2e": "npm run build && vitest run src/e2e.test.ts",
37
- "test:watch": "vitest"
37
+ "test:watch": "vitest",
38
+ "prepare": "husky"
38
39
  },
39
40
  "dependencies": {
40
41
  "@inquirer/prompts": "^8.3.0",
@@ -49,11 +50,20 @@
49
50
  "engines": {
50
51
  "node": ">=18"
51
52
  },
53
+ "lint-staged": {
54
+ "*.{ts,js}": [
55
+ "prettier --write",
56
+ "eslint --fix"
57
+ ],
58
+ "*.{json,md,yml}": "prettier --write"
59
+ },
52
60
  "devDependencies": {
53
61
  "@eslint/js": "^10.0.1",
54
62
  "@types/node": "^22.13.5",
55
63
  "eslint": "^10.0.2",
56
64
  "eslint-config-prettier": "^10.1.8",
65
+ "husky": "^9.1.7",
66
+ "lint-staged": "^16.2.7",
57
67
  "prettier": "^3.8.1",
58
68
  "typescript": "^5.7.3",
59
69
  "typescript-eslint": "^8.56.1",
File without changes
File without changes