backdot 1.8.0 → 1.8.2

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/dist/launchd.js CHANGED
@@ -5,8 +5,9 @@ import os from "node:os";
5
5
  import ora from "ora";
6
6
  import { logger } from "./log.js";
7
7
  import { errorMessage } from "./utils.js";
8
- function escapeXml(s) {
9
- return s
8
+ import { LOG_DIR, LAUNCHD_LOG_PATH } from "./paths.js";
9
+ function escapeXml(text) {
10
+ return text
10
11
  .replace(/&/g, "&")
11
12
  .replace(/</g, "&lt;")
12
13
  .replace(/>/g, "&gt;")
@@ -22,7 +23,6 @@ function buildPlist() {
22
23
  const nodePath = process.execPath;
23
24
  const scriptPath = getScriptPath();
24
25
  const workingDir = path.dirname(scriptPath);
25
- const logPath = path.join(os.homedir(), ".backdot", "launchd.log");
26
26
  return `<?xml version="1.0" encoding="UTF-8"?>
27
27
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
28
  <plist version="1.0">
@@ -45,9 +45,9 @@ function buildPlist() {
45
45
  <integer>0</integer>
46
46
  </dict>
47
47
  <key>StandardOutPath</key>
48
- <string>${escapeXml(logPath)}</string>
48
+ <string>${escapeXml(LAUNCHD_LOG_PATH)}</string>
49
49
  <key>StandardErrorPath</key>
50
- <string>${escapeXml(logPath)}</string>
50
+ <string>${escapeXml(LAUNCHD_LOG_PATH)}</string>
51
51
  <key>RunAtLoad</key>
52
52
  <false/>
53
53
  </dict>
@@ -72,9 +72,8 @@ export function setupLaunchd() {
72
72
  const spinner = ora("Installing schedule").start();
73
73
  const plistContent = buildPlist();
74
74
  const launchAgentsDir = path.dirname(PLIST_PATH);
75
- if (!fs.existsSync(launchAgentsDir)) {
76
- fs.mkdirSync(launchAgentsDir, { recursive: true });
77
- }
75
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
76
+ fs.mkdirSync(LOG_DIR, { recursive: true });
78
77
  try {
79
78
  execFileSync("launchctl", ["unload", PLIST_PATH], { stdio: "pipe" });
80
79
  }
package/dist/log.js CHANGED
@@ -1,9 +1,6 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
2
  import winston from "winston";
5
- const LOG_DIR = path.join(os.homedir(), ".backdot");
6
- const LOG_FILE = path.join(LOG_DIR, "backup.log");
3
+ import { LOG_DIR, CLI_LOG_PATH } from "./paths.js";
7
4
  let _logger;
8
5
  function getLogger() {
9
6
  if (!_logger) {
@@ -11,7 +8,7 @@ function getLogger() {
11
8
  _logger = winston.createLogger({
12
9
  level: "info",
13
10
  format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.printf(({ timestamp, level, message }) => `${timestamp} [${level}] ${message}`)),
14
- transports: [new winston.transports.File({ filename: LOG_FILE })],
11
+ transports: [new winston.transports.File({ filename: CLI_LOG_PATH })],
15
12
  });
16
13
  }
17
14
  return _logger;
package/dist/notify.js CHANGED
@@ -8,12 +8,12 @@ export function sendNotification(title, message) {
8
8
  if (process.platform !== "darwin") {
9
9
  return;
10
10
  }
11
- const escaped = escapeAppleScript(message);
12
- const titleEscaped = escapeAppleScript(title);
11
+ const escapedMessage = escapeAppleScript(message);
12
+ const escapedTitle = escapeAppleScript(title);
13
13
  try {
14
14
  execFileSync("osascript", [
15
15
  "-e",
16
- `display notification "${escaped}" with title "${titleEscaped}" subtitle "Scheduled backup failed"`,
16
+ `display notification "${escapedMessage}" with title "${escapedTitle}" subtitle "Scheduled backup failed"`,
17
17
  ], { stdio: "pipe" });
18
18
  }
19
19
  catch (err) {
package/dist/paths.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export declare const CONFIG_PATH: string;
2
+ export declare const KEY_FILE_PATH: string;
3
+ export declare const POST_RESTORE_HOOK_PATH: string;
1
4
  export declare const STAGING_DIR: string;
2
5
  export declare const STAGING_GIT_DIR: string;
6
+ export declare const LOG_DIR: string;
7
+ export declare const CLI_LOG_PATH: string;
8
+ export declare const LAUNCHD_LOG_PATH: string;
3
9
  export declare function machineDir(machine: string): string;
package/dist/paths.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
3
  const HOME = os.homedir();
4
- export const STAGING_DIR = path.join(HOME, ".backdot", "repo");
4
+ const BACKDOT_DIR = path.join(HOME, ".backdot");
5
+ export const CONFIG_PATH = path.join(BACKDOT_DIR, "config.json");
6
+ export const KEY_FILE_PATH = path.join(BACKDOT_DIR, "encryption.key");
7
+ export const POST_RESTORE_HOOK_PATH = path.join(BACKDOT_DIR, "post-restore");
8
+ export const STAGING_DIR = path.join(BACKDOT_DIR, "repo");
5
9
  export const STAGING_GIT_DIR = path.join(STAGING_DIR, ".git");
10
+ export const LOG_DIR = path.join(BACKDOT_DIR, "logs");
11
+ export const CLI_LOG_PATH = path.join(LOG_DIR, "cli.log");
12
+ export const LAUNCHD_LOG_PATH = path.join(LOG_DIR, "launchd.log");
6
13
  export function machineDir(machine) {
7
14
  return path.join(STAGING_DIR, machine);
8
15
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs the restored `~/.backdot/post-restore` script (a POSIX `sh` script) so a
3
+ * restored machine can provision itself. The caller decides when to invoke this
4
+ * (only when the hook was actually among the restored files). A non-zero exit is
5
+ * surfaced as an error — never swallowed — but files have already been restored.
6
+ */
7
+ export declare function runPostRestoreHook(): void;
@@ -0,0 +1,35 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import os from "node:os";
3
+ import { logger } from "./log.js";
4
+ import { errorMessage } from "./utils.js";
5
+ import { POST_RESTORE_HOOK_PATH } from "./paths.js";
6
+ /**
7
+ * Runs the restored `~/.backdot/post-restore` script (a POSIX `sh` script) so a
8
+ * restored machine can provision itself. The caller decides when to invoke this
9
+ * (only when the hook was actually among the restored files). A non-zero exit is
10
+ * surfaced as an error — never swallowed — but files have already been restored.
11
+ */
12
+ export function runPostRestoreHook() {
13
+ if (process.platform === "win32") {
14
+ console.log(" Post-restore hook found, but hooks are not supported on Windows. Skipping.\n");
15
+ logger.warn("Post-restore hook skipped (not supported on Windows)");
16
+ return;
17
+ }
18
+ console.log(` Running post-restore hook (${POST_RESTORE_HOOK_PATH})…\n`);
19
+ logger.info("Running post-restore hook");
20
+ try {
21
+ execFileSync("/bin/sh", [POST_RESTORE_HOOK_PATH], {
22
+ cwd: os.homedir(),
23
+ stdio: "inherit",
24
+ });
25
+ }
26
+ catch (err) {
27
+ logger.error(`Post-restore hook failed: ${errorMessage(err)}`);
28
+ const exitCode = err.status;
29
+ const exitNote = typeof exitCode === "number" ? ` (exit code ${exitCode})` : "";
30
+ throw new Error(`Files were restored, but the post-restore hook failed${exitNote}.\n` +
31
+ ` Fix ${POST_RESTORE_HOOK_PATH} and re-run, or run it manually.`, { cause: err });
32
+ }
33
+ console.log();
34
+ logger.info("Post-restore hook completed");
35
+ }
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
- const KNOWN_HOSTS = ["github.com", "gitlab.com", "bitbucket.org"];
2
+ import { extractRepoPath } from "./utils.js";
3
3
  /**
4
4
  * Converts an SSH or HTTPS repo URL to a credential-free HTTPS URL
5
5
  * for known hosts. Returns `null` for unrecognized hosts.
@@ -9,18 +9,11 @@ const KNOWN_HOSTS = ["github.com", "gitlab.com", "bitbucket.org"];
9
9
  * https://github.com/user/repo → https://github.com/user/repo.git
10
10
  */
11
11
  export function toHttpsUrl(repository) {
12
- for (const host of KNOWN_HOSTS) {
13
- const idx = repository.indexOf(host);
14
- if (idx === -1) {
15
- continue;
16
- }
17
- let repoPath = repository.slice(idx + host.length + 1).trim();
18
- if (!repoPath.endsWith(".git")) {
19
- repoPath += ".git";
20
- }
21
- return `https://${host}/${repoPath}`;
12
+ const parsed = extractRepoPath(repository);
13
+ if (!parsed) {
14
+ return null;
22
15
  }
23
- return null;
16
+ return `https://${parsed.host}/${parsed.repoPath}.git`;
24
17
  }
25
18
  /**
26
19
  * Checks whether `repository` is publicly readable by attempting an
@@ -43,7 +36,12 @@ function execGitLsRemote(url) {
43
36
  return new Promise((resolve, reject) => {
44
37
  const child = execFile("git", ["-c", "credential.helper=", "ls-remote", "--quiet", url], {
45
38
  timeout: 10_000,
46
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
39
+ env: {
40
+ ...process.env,
41
+ GIT_TERMINAL_PROMPT: "0",
42
+ GIT_ASKPASS: undefined,
43
+ SSH_ASKPASS: undefined,
44
+ },
47
45
  }, (error) => {
48
46
  if (error) {
49
47
  reject(error);
package/dist/staging.d.ts CHANGED
@@ -1,19 +1,33 @@
1
1
  import { STAGING_DIR, STAGING_GIT_DIR, machineDir } from "./paths.js";
2
2
  import { type DerivedKey } from "./crypto/encryption.js";
3
3
  export { STAGING_DIR, STAGING_GIT_DIR, machineDir };
4
+ export declare const HOME_NAMESPACE = "home";
5
+ export declare const ROOT_NAMESPACE = "root";
4
6
  export declare function getStagedPath(filePath: string, machine: string): string;
7
+ export interface RestoreTarget {
8
+ /** Absolute path the file is restored to on this machine. */
9
+ destination: string;
10
+ /** Path shown in the restore picker (e.g. "~/.zshrc" or "/etc/hosts"). */
11
+ displayPath: string;
12
+ }
13
+ /**
14
+ * Inverse of getStagedPath: maps a path relative to the machine dir
15
+ * (e.g. "home/.zshrc" or "root/etc/hosts") back to its restore destination.
16
+ */
17
+ export declare function getRestoreTarget(machineRelativePath: string): RestoreTarget;
5
18
  export declare function cleanStaging(machine: string): void;
6
19
  export declare function copyToStaging(files: string[], machine: string, derivedKey?: DerivedKey): void;
7
20
  export interface ComparisonResult {
8
21
  backedUp: string[];
9
22
  modified: string[];
10
23
  notBackedUp: string[];
24
+ remoteIsEmpty?: boolean;
11
25
  error?: string;
12
26
  }
13
27
  export declare function compareFiles(opts: {
14
28
  files: string[];
15
29
  machine: string;
16
30
  repository: string;
17
- derivedKey?: DerivedKey;
31
+ resolveKey?: () => Promise<DerivedKey>;
18
32
  }): Promise<ComparisonResult>;
19
33
  export declare function writeRepoReadme(repository: string, encrypted?: boolean): void;
package/dist/staging.js CHANGED
@@ -11,20 +11,44 @@ import { encrypt, decrypt } from "./crypto/encryption.js";
11
11
  import { KEY_FILE_PATH, ENC_SUFFIX } from "./crypto/password.js";
12
12
  export { STAGING_DIR, STAGING_GIT_DIR, machineDir };
13
13
  const HOME = os.homedir();
14
+ // Backed-up files live under one of two namespaces inside the machine dir, which
15
+ // keeps the layout lossless: HOME files restore relative to the restoring machine's
16
+ // own home (portable across machines), while files elsewhere restore to their
17
+ // original absolute path.
18
+ export const HOME_NAMESPACE = "home";
19
+ export const ROOT_NAMESPACE = "root";
20
+ function isOutsideHome(filePath) {
21
+ const relativeToHome = path.relative(HOME, filePath);
22
+ return relativeToHome.startsWith("..") || path.isAbsolute(relativeToHome);
23
+ }
14
24
  export function getStagedPath(filePath, machine) {
15
- const relativePath = path.relative(HOME, filePath);
16
- // Files outside HOME (e.g. /etc/foo) produce a relative path starting with "..",
17
- // which would escape the machine dir. Use the absolute path minus the leading "/" instead.
18
- const pathWithinMachineDir = relativePath.startsWith("..") ? filePath.slice(1) : relativePath;
25
+ const pathWithinMachineDir = isOutsideHome(filePath)
26
+ ? path.join(ROOT_NAMESPACE, filePath.slice(1)) // strip leading "/" so it stays inside the machine dir
27
+ : path.join(HOME_NAMESPACE, path.relative(HOME, filePath));
19
28
  return path.join(machineDir(machine), pathWithinMachineDir);
20
29
  }
30
+ /**
31
+ * Inverse of getStagedPath: maps a path relative to the machine dir
32
+ * (e.g. "home/.zshrc" or "root/etc/hosts") back to its restore destination.
33
+ */
34
+ export function getRestoreTarget(machineRelativePath) {
35
+ const [namespace, ...rest] = machineRelativePath.split(path.sep);
36
+ const subPath = rest.join(path.sep);
37
+ if (namespace === ROOT_NAMESPACE) {
38
+ const destination = path.join("/", subPath);
39
+ return { destination, displayPath: destination };
40
+ }
41
+ return { destination: path.join(HOME, subPath), displayPath: path.join("~", subPath) };
42
+ }
21
43
  export function cleanStaging(machine) {
22
- const machineStagingDir = machineDir(machine);
23
- if (!fs.existsSync(machineStagingDir)) {
24
- return;
44
+ // Remove only the backdot-managed namespaces so user-authored files in the
45
+ // machine dir (e.g. a hand-written README.md with restore notes) survive
46
+ // across backups. home/ and root/ are still fully rebuilt, so a backup
47
+ // remains a complete snapshot of the configured files.
48
+ for (const namespace of [HOME_NAMESPACE, ROOT_NAMESPACE]) {
49
+ fs.rmSync(path.join(machineDir(machine), namespace), { recursive: true, force: true });
25
50
  }
26
- fs.rmSync(machineStagingDir, { recursive: true, force: true });
27
- logger.info(`Cleaned staging directory for machine "${machine}"`);
51
+ logger.info(`Cleaned staging namespaces for machine "${machine}"`);
28
52
  }
29
53
  export function copyToStaging(files, machine, derivedKey) {
30
54
  const machineStagingDir = machineDir(machine);
@@ -54,13 +78,18 @@ export function copyToStaging(files, machine, derivedKey) {
54
78
  function failedComparisonResult(err) {
55
79
  return { backedUp: [], modified: [], notBackedUp: [], error: errorMessage(err) };
56
80
  }
81
+ // This machine has no snapshot in the remote yet, so every file counts as "not
82
+ // backed up". Lets `status` preview what a first backup would push instead of erroring.
83
+ function emptyRemoteResult(files) {
84
+ return { backedUp: [], modified: [], notBackedUp: files, remoteIsEmpty: true };
85
+ }
57
86
  export async function compareFiles(opts) {
58
- const { files, machine, repository, derivedKey } = opts;
87
+ const { files, machine, repository, resolveKey } = opts;
59
88
  if (files.length === 0) {
60
89
  return { backedUp: [], modified: [], notBackedUp: [] };
61
90
  }
62
91
  if (!fs.existsSync(STAGING_GIT_DIR)) {
63
- return failedComparisonResult(new Error('Backup repository not found. Run "backdot backup" first.'));
92
+ return emptyRemoteResult(files);
64
93
  }
65
94
  const git = simpleGit(STAGING_DIR);
66
95
  try {
@@ -92,8 +121,27 @@ export async function compareFiles(opts) {
92
121
  catch (err) {
93
122
  return failedComparisonResult(err);
94
123
  }
95
- if (derivedKey) {
96
- return compareFilesEncrypted(files, machine, remoteBlobHashes, derivedKey);
124
+ // Repo reachable but this machine has nothing backed up yet (empty repo, or it
125
+ // only holds other machines). Treat as a pre-backup preview.
126
+ if (remoteBlobHashes.size === 0) {
127
+ return emptyRemoteResult(files);
128
+ }
129
+ if (resolveKey) {
130
+ const derivedKey = await resolveKey();
131
+ return compareFilesToRemote(files, machine, remoteBlobHashes, ENC_SUFFIX, (file, remoteBlobHash) => {
132
+ try {
133
+ const blobContent = execFileSync("git", ["cat-file", "blob", remoteBlobHash], {
134
+ cwd: STAGING_DIR,
135
+ maxBuffer: 50 * 1024 * 1024,
136
+ });
137
+ const decrypted = decrypt(blobContent, derivedKey);
138
+ const localContent = fs.readFileSync(file);
139
+ return decrypted.equals(localContent);
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ });
97
145
  }
98
146
  let localFileHashes;
99
147
  try {
@@ -106,45 +154,23 @@ export async function compareFiles(opts) {
106
154
  catch (err) {
107
155
  return failedComparisonResult(err);
108
156
  }
109
- return files.reduce((result, file, i) => {
110
- const repoRelPath = path.relative(STAGING_DIR, getStagedPath(file, machine));
111
- const remoteBlobHash = remoteBlobHashes.get(repoRelPath);
112
- if (!remoteBlobHash) {
113
- result.notBackedUp.push(file);
114
- }
115
- else if (remoteBlobHash === localFileHashes[i]) {
116
- result.backedUp.push(file);
117
- }
118
- else {
119
- result.modified.push(file);
120
- }
121
- return result;
122
- }, { backedUp: [], modified: [], notBackedUp: [] });
157
+ const localHashByFile = new Map(files.map((file, i) => [file, localFileHashes[i]]));
158
+ return compareFilesToRemote(files, machine, remoteBlobHashes, "", (file, remoteBlobHash) => {
159
+ return remoteBlobHash === localHashByFile.get(file);
160
+ });
123
161
  }
124
- function compareFilesEncrypted(files, machine, remoteBlobHashes, derivedKey) {
162
+ function compareFilesToRemote(files, machine, remoteBlobHashes, pathSuffix, matchesRemote) {
125
163
  const result = { backedUp: [], modified: [], notBackedUp: [] };
126
164
  for (const file of files) {
127
- const repoRelPath = path.relative(STAGING_DIR, getStagedPath(file, machine)) + ENC_SUFFIX;
128
- const remoteBlobHash = remoteBlobHashes.get(repoRelPath);
165
+ const repoRelativePath = path.relative(STAGING_DIR, getStagedPath(file, machine)) + pathSuffix;
166
+ const remoteBlobHash = remoteBlobHashes.get(repoRelativePath);
129
167
  if (!remoteBlobHash) {
130
168
  result.notBackedUp.push(file);
131
- continue;
132
169
  }
133
- try {
134
- const blobContent = execFileSync("git", ["cat-file", "blob", remoteBlobHash], {
135
- cwd: STAGING_DIR,
136
- maxBuffer: 50 * 1024 * 1024,
137
- });
138
- const decrypted = decrypt(blobContent, derivedKey);
139
- const localContent = fs.readFileSync(file);
140
- if (decrypted.equals(localContent)) {
141
- result.backedUp.push(file);
142
- }
143
- else {
144
- result.modified.push(file);
145
- }
170
+ else if (matchesRemote(file, remoteBlobHash)) {
171
+ result.backedUp.push(file);
146
172
  }
147
- catch {
173
+ else {
148
174
  result.modified.push(file);
149
175
  }
150
176
  }
@@ -161,9 +187,11 @@ ${encryptionNote}
161
187
  ## Restore
162
188
 
163
189
  \`\`\`bash
164
- npx backdot restore ${repository}
190
+ npx backdot restore ${repository} --machine <machine>
165
191
  \`\`\`
166
192
 
193
+ Each machine is a top-level directory here; replace \`<machine>\` with the one you want to restore. A machine may also contain its own \`README.md\` with extra, machine-specific restore steps — backdot preserves it across backups.
194
+
167
195
  For full documentation, configuration options, and scheduling, see the [official README](https://github.com/sorenlouv/backdot).
168
196
  `;
169
197
  }
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,11 @@
1
1
  export declare function errorMessage(err: unknown): string;
2
+ /**
3
+ * Extracts the repo path (e.g. "user/repo") from an SSH or HTTPS URL
4
+ * for known hosts. Returns `null` for unrecognized hosts.
5
+ */
6
+ export declare function extractRepoPath(url: string): {
7
+ host: string;
8
+ repoPath: string;
9
+ } | null;
2
10
  export declare function uniq<T>(items: T[]): T[];
3
11
  export declare function pluralize(count: number, word: string): string;
package/dist/utils.js CHANGED
@@ -1,6 +1,26 @@
1
1
  export function errorMessage(err) {
2
2
  return err instanceof Error ? err.message : String(err);
3
3
  }
4
+ const KNOWN_HOSTS = ["github.com", "gitlab.com", "bitbucket.org"];
5
+ /**
6
+ * Extracts the repo path (e.g. "user/repo") from an SSH or HTTPS URL
7
+ * for known hosts. Returns `null` for unrecognized hosts.
8
+ */
9
+ export function extractRepoPath(url) {
10
+ for (const host of KNOWN_HOSTS) {
11
+ const idx = url.indexOf(host);
12
+ if (idx === -1) {
13
+ continue;
14
+ }
15
+ const separatorLength = 1; // skip ":" (SSH) or "/" (HTTPS) after hostname
16
+ let repoPath = url.slice(idx + host.length + separatorLength).trim();
17
+ if (repoPath.endsWith(".git")) {
18
+ repoPath = repoPath.slice(0, -4);
19
+ }
20
+ return { host, repoPath };
21
+ }
22
+ return null;
23
+ }
4
24
  export function uniq(items) {
5
25
  return [...new Set(items)];
6
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backdot",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
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",
@@ -26,42 +26,42 @@
26
26
  "scripts": {
27
27
  "build": "tsc",
28
28
  "build:watch": "tsc --watch",
29
- "release": "./scripts/release.sh",
30
- "start": "node dist/cli.js",
29
+ "fmt": "prettier --write src/",
30
+ "fmt:check": "prettier --check src/",
31
31
  "lint": "eslint src/",
32
32
  "lint:fix": "eslint src/ --fix",
33
- "format": "prettier --write src/",
34
- "format:check": "prettier --check src/",
33
+ "prepare": "cd .. && husky",
34
+ "start": "node dist/cli.js",
35
35
  "test": "vitest run --exclude src/e2e.test.ts --exclude src/git.integration.test.ts",
36
- "test:integration": "vitest run src/git.integration.test.ts",
37
- "test:e2e": "npm run build && vitest run src/e2e.test.ts",
38
36
  "test:all": "npm run build && vitest run",
39
- "test:watch": "vitest",
40
- "prepare": "husky"
37
+ "test:e2e": "npm run build && vitest run src/e2e.test.ts",
38
+ "test:integration": "vitest run src/git.integration.test.ts",
39
+ "test:watch": "vitest"
41
40
  },
42
41
  "dependencies": {
43
- "@inquirer/prompts": "^8.3.0",
42
+ "@inquirer/prompts": "^8.5.2",
44
43
  "cac": "^7.0.0",
45
44
  "chalk": "^5.6.2",
46
45
  "fast-glob": "^3.3.3",
47
- "ora": "^9.3.0",
48
- "p-retry": "^7.1.1",
49
- "simple-git": "^3.32.2",
46
+ "ora": "^9.4.0",
47
+ "p-retry": "^8.0.0",
48
+ "simple-git": "^3.36.0",
50
49
  "winston": "^3.19.0",
51
- "zod": "^4.3.6"
50
+ "zod": "^4.4.3"
52
51
  },
53
52
  "engines": {
54
- "node": ">=18"
53
+ "node": ">=24"
55
54
  },
56
55
  "devDependencies": {
57
56
  "@eslint/js": "^10.0.1",
58
- "@types/node": "^22.13.5",
59
- "eslint": "^10.0.2",
57
+ "@types/node": "^25.9.3",
58
+ "esbuild": "^0.28.1",
59
+ "eslint": "^10.5.0",
60
60
  "eslint-config-prettier": "^10.1.8",
61
61
  "husky": "^9.1.7",
62
- "prettier": "^3.8.1",
63
- "typescript": "^5.7.3",
64
- "typescript-eslint": "^8.56.1",
65
- "vitest": "^4.0.18"
62
+ "prettier": "^3.8.4",
63
+ "typescript": "^6.0.3",
64
+ "typescript-eslint": "^8.61.0",
65
+ "vitest": "^4.1.8"
66
66
  }
67
67
  }
package/README.md DELETED
@@ -1,82 +0,0 @@
1
- <p align="center">
2
- <img src="logo-small.png" alt="backdot" width="400" />
3
- </p>
4
-
5
- <h1 align="center">backdot</h1>
6
-
7
- <p align="center">Automated backup of important files (configs, dotfiles) to your own private Git repo.</p>
8
-
9
- ## Getting started
10
-
11
- ```bash
12
- npm install -g backdot
13
- backdot init
14
- ```
15
-
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:
17
-
18
- ```json
19
- {
20
- "repository": "git@github.com:USERNAME/backdot-backup.git",
21
- "machine": "my-work-laptop",
22
- "paths": ["~/.zshrc", "~/.oh-my-zsh/custom/*.zsh", "~/.ssh/config", "~/.npmrc"]
23
- }
24
- ```
25
-
26
- Run your first backup:
27
-
28
- ```bash
29
- backdot backup
30
- ```
31
-
32
- or configure the backport process to run automatically (daily at 2am)
33
-
34
- ```bash
35
- backdot schedule
36
- ```
37
-
38
- ## Configuration
39
-
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
- ```
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
-
58
- ## Commands
59
-
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 |
71
-
72
- ## Development
73
-
74
- ```bash
75
- npm install
76
- npm run build
77
- npm start
78
- ```
79
-
80
- ## License
81
-
82
- MIT
package/dist/crypto.d.ts DELETED
@@ -1,13 +0,0 @@
1
- export declare const KEY_FILE_PATH: string;
2
- export declare const ENC_SUFFIX = ".encrypted";
3
- export declare function encryptBuffer(plaintext: Buffer, password: string): Buffer;
4
- export declare function decryptBuffer(data: Buffer, password: string): Buffer;
5
- export declare function checkKeyFilePermissions(): void;
6
- export declare function saveKeyFile(password: string): void;
7
- export interface PasswordResult {
8
- password: string;
9
- interactive: boolean;
10
- }
11
- export declare function resolvePassword(): Promise<PasswordResult>;
12
- export declare function confirmPassword(password: string): Promise<void>;
13
- export declare function offerToSaveKeyFile(password: string): Promise<void>;