backdot 1.4.0 → 1.6.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 +23 -14
- package/dist/cli.js +65 -134
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +40 -0
- package/dist/commands/history.d.ts +1 -0
- package/dist/commands/history.js +28 -0
- package/dist/{init.js → commands/init.js} +1 -2
- package/dist/commands/restore.d.ts +5 -0
- package/dist/{restore.js → commands/restore.js} +32 -27
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +14 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +69 -0
- package/dist/config.d.ts +1 -2
- package/dist/config.js +7 -8
- package/dist/git.d.ts +17 -1
- package/dist/git.js +64 -7
- package/dist/log.js +4 -1
- package/dist/notify.js +10 -4
- package/dist/plist.js +6 -6
- package/dist/resolve.js +5 -30
- package/dist/staging.d.ts +1 -0
- package/dist/staging.js +19 -14
- package/package.json +12 -2
- package/dist/restore.d.ts +0 -1
- /package/dist/{init.d.ts → commands/init.d.ts} +0 -0
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
|
|
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,21 +37,31 @@ backdot --schedule
|
|
|
38
37
|
|
|
39
38
|
## Configuration
|
|
40
39
|
|
|
41
|
-
| Key
|
|
42
|
-
|
|
|
43
|
-
| `
|
|
44
|
-
|
|
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
|
|
|
48
|
-
| Command
|
|
49
|
-
|
|
|
50
|
-
| `--init`
|
|
51
|
-
| `--backup`
|
|
52
|
-
| `--restore`
|
|
53
|
-
| `--
|
|
54
|
-
| `--
|
|
55
|
-
| `--
|
|
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 |
|
|
56
65
|
|
|
57
66
|
## Development
|
|
58
67
|
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
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";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { init } from "./init.js";
|
|
13
|
-
import { setupLaunchd, uninstallLaunchd, isScheduled } from "./plist.js";
|
|
6
|
+
import { CONFIG_PATH } from "./config.js";
|
|
7
|
+
import { backup } from "./commands/backup.js";
|
|
8
|
+
import { status } from "./commands/status.js";
|
|
9
|
+
import { schedule, unschedule } from "./commands/schedule.js";
|
|
10
|
+
import { restore } from "./commands/restore.js";
|
|
11
|
+
import { history } from "./commands/history.js";
|
|
12
|
+
import { init } from "./commands/init.js";
|
|
14
13
|
import { logger } from "./log.js";
|
|
15
14
|
import { sendNotification } from "./notify.js";
|
|
16
15
|
function getVersion() {
|
|
@@ -23,151 +22,83 @@ function getVersion() {
|
|
|
23
22
|
return "unknown";
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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();
|
|
25
|
+
function printHelp() {
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(" Usage: backdot <command>");
|
|
66
28
|
console.log();
|
|
67
|
-
console.log(
|
|
29
|
+
console.log(" Commands:");
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(" --init Set up backdot for the first time");
|
|
32
|
+
console.log(" --backup Run a backup now");
|
|
33
|
+
console.log(" --restore [url] Restore files from the backup repo");
|
|
34
|
+
console.log(" --restore [url] --commit <sha> Restore from a specific backup commit");
|
|
35
|
+
console.log(" --restore [url] --yes (-y) Accept defaults without prompting");
|
|
36
|
+
console.log(" --history [url] Browse and restore a previous backup");
|
|
37
|
+
console.log(" --schedule Install daily backup schedule (macOS launchd)");
|
|
38
|
+
console.log(" --unschedule Remove the daily backup schedule");
|
|
39
|
+
console.log(" --status Show schedule and resolved files");
|
|
40
|
+
console.log(" --version Show version");
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
43
|
+
async function main() {
|
|
44
|
+
let values;
|
|
45
|
+
let positionals;
|
|
68
46
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
47
|
+
({ values, positionals } = parseArgs({
|
|
48
|
+
args: process.argv.slice(2),
|
|
49
|
+
options: {
|
|
50
|
+
init: { type: "boolean" },
|
|
51
|
+
backup: { type: "boolean" },
|
|
52
|
+
restore: { type: "boolean" },
|
|
53
|
+
history: { type: "boolean" },
|
|
54
|
+
schedule: { type: "boolean" },
|
|
55
|
+
unschedule: { type: "boolean" },
|
|
56
|
+
status: { type: "boolean" },
|
|
57
|
+
version: { type: "boolean" },
|
|
58
|
+
help: { type: "boolean" },
|
|
59
|
+
commit: { type: "string" },
|
|
60
|
+
yes: { type: "boolean", short: "y" },
|
|
61
|
+
},
|
|
62
|
+
allowPositionals: true,
|
|
63
|
+
strict: true,
|
|
64
|
+
}));
|
|
111
65
|
}
|
|
112
66
|
catch (err) {
|
|
113
67
|
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
-
console.error(`\n
|
|
68
|
+
console.error(`\n Error: ${msg}\n`);
|
|
69
|
+
printHelp();
|
|
115
70
|
process.exit(1);
|
|
116
71
|
}
|
|
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
|
-
async function main() {
|
|
125
|
-
const args = process.argv.slice(2);
|
|
126
|
-
const command = args[0];
|
|
127
72
|
try {
|
|
128
|
-
if (
|
|
73
|
+
if (values.init) {
|
|
129
74
|
init();
|
|
130
75
|
}
|
|
131
|
-
else if (
|
|
76
|
+
else if (values.backup) {
|
|
132
77
|
await backup();
|
|
133
78
|
}
|
|
134
|
-
else if (
|
|
135
|
-
|
|
136
|
-
setupLaunchd();
|
|
79
|
+
else if (values.schedule) {
|
|
80
|
+
schedule();
|
|
137
81
|
}
|
|
138
|
-
else if (
|
|
139
|
-
|
|
140
|
-
uninstallLaunchd();
|
|
82
|
+
else if (values.unschedule) {
|
|
83
|
+
unschedule();
|
|
141
84
|
}
|
|
142
|
-
else if (
|
|
85
|
+
else if (values.status) {
|
|
143
86
|
await status();
|
|
144
87
|
}
|
|
145
|
-
else if (
|
|
146
|
-
await restore(
|
|
88
|
+
else if (values.restore) {
|
|
89
|
+
await restore(positionals[0], values.commit, {
|
|
90
|
+
yes: !!values.yes,
|
|
91
|
+
});
|
|
147
92
|
}
|
|
148
|
-
else if (
|
|
93
|
+
else if (values.history) {
|
|
94
|
+
await history(positionals[0]);
|
|
95
|
+
}
|
|
96
|
+
else if (values.version) {
|
|
149
97
|
console.log(getVersion());
|
|
150
98
|
}
|
|
151
99
|
else {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log();
|
|
155
|
-
console.log(" Commands:");
|
|
156
|
-
console.log();
|
|
157
|
-
console.log(" --init Set up backdot for the first time");
|
|
158
|
-
console.log(" --backup Run a backup now");
|
|
159
|
-
console.log(" --restore [url] Restore files from the backup repo");
|
|
160
|
-
console.log(" --schedule Install daily backup schedule (macOS launchd)");
|
|
161
|
-
console.log(" --unschedule Remove the daily backup schedule");
|
|
162
|
-
console.log(" --status Show schedule and resolved files");
|
|
163
|
-
console.log(" --version Show version");
|
|
164
|
-
console.log();
|
|
165
|
-
if (command && command !== "--help") {
|
|
166
|
-
console.error(` Unknown command: ${command}`);
|
|
167
|
-
console.log();
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
else if (!fs.existsSync(CONFIG_PATH)) {
|
|
100
|
+
printHelp();
|
|
101
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
171
102
|
console.log(` No config found. Run ${chalk.bold("backdot --init")} to get started.`);
|
|
172
103
|
console.log();
|
|
173
104
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function history(repoUrl?: string): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { select } from "@inquirer/prompts";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { gitPull, gitLog } from "../git.js";
|
|
5
|
+
import { restore } from "./restore.js";
|
|
6
|
+
import { logger } from "../log.js";
|
|
7
|
+
export async function history(repoUrl) {
|
|
8
|
+
logger.info("Starting history");
|
|
9
|
+
const repository = repoUrl ?? loadConfig().repository;
|
|
10
|
+
const spinner = ora("Fetching backup history").start();
|
|
11
|
+
await gitPull(repository);
|
|
12
|
+
const commits = await gitLog();
|
|
13
|
+
spinner.stop();
|
|
14
|
+
if (commits.length === 0) {
|
|
15
|
+
console.log("\n No backup history found.\n");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const selected = await select({
|
|
19
|
+
message: "Select a backup to restore from:",
|
|
20
|
+
loop: false,
|
|
21
|
+
choices: commits.map((c) => ({
|
|
22
|
+
name: `${c.hash.slice(0, 7)} ${c.date.split("T")[0]} ${c.message}`,
|
|
23
|
+
value: c.hash,
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
console.log();
|
|
27
|
+
await restore(repoUrl, selected);
|
|
28
|
+
}
|
|
@@ -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 "
|
|
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() {
|
|
@@ -2,11 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import ora from "ora";
|
|
5
|
-
import { checkbox, select } from "@inquirer/prompts";
|
|
6
|
-
import { loadConfig } from "
|
|
7
|
-
import { gitPull } from "
|
|
8
|
-
import { STAGING_DIR, machineDir } from "
|
|
9
|
-
import { logger } from "
|
|
5
|
+
import { checkbox, select, Separator } 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";
|
|
10
10
|
const HOME = os.homedir();
|
|
11
11
|
function walkDir(dir) {
|
|
12
12
|
return fs
|
|
@@ -25,13 +25,13 @@ function listMachines() {
|
|
|
25
25
|
.filter((e) => e.isDirectory() && e.name !== ".git")
|
|
26
26
|
.map((e) => e.name);
|
|
27
27
|
}
|
|
28
|
-
async function resolveRepoAndMachine(repoUrl) {
|
|
28
|
+
async function resolveRepoAndMachine(repoUrl, commit) {
|
|
29
29
|
if (!repoUrl) {
|
|
30
30
|
const config = loadConfig();
|
|
31
31
|
return { repository: config.repository, machine: config.machine };
|
|
32
32
|
}
|
|
33
33
|
const spinner = ora("Cloning backup repository").start();
|
|
34
|
-
await gitPull(repoUrl);
|
|
34
|
+
await gitPull(repoUrl, commit);
|
|
35
35
|
spinner.stop();
|
|
36
36
|
const machines = listMachines();
|
|
37
37
|
if (machines.length === 0) {
|
|
@@ -44,18 +44,19 @@ async function resolveRepoAndMachine(repoUrl) {
|
|
|
44
44
|
else {
|
|
45
45
|
machine = await select({
|
|
46
46
|
message: "Multiple machines found. Which one do you want to restore?",
|
|
47
|
+
loop: false,
|
|
47
48
|
choices: machines.map((m) => ({ name: m, value: m })),
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
return { repository: repoUrl, machine };
|
|
51
52
|
}
|
|
52
|
-
export async function restore(repoUrl) {
|
|
53
|
+
export async function restore(repoUrl, commit, options = {}) {
|
|
53
54
|
logger.info("Starting restore");
|
|
54
|
-
const { repository, machine } = await resolveRepoAndMachine(repoUrl);
|
|
55
|
+
const { repository, machine } = await resolveRepoAndMachine(repoUrl, commit);
|
|
55
56
|
const spinner = ora("Fetching latest backup").start();
|
|
56
57
|
const baseDir = machineDir(machine);
|
|
57
58
|
if (!repoUrl) {
|
|
58
|
-
await gitPull(repository);
|
|
59
|
+
await gitPull(repository, commit);
|
|
59
60
|
}
|
|
60
61
|
spinner.text = "Resolving files";
|
|
61
62
|
if (!fs.existsSync(baseDir)) {
|
|
@@ -86,25 +87,29 @@ export async function restore(repoUrl) {
|
|
|
86
87
|
logger.info(`${fresh.length} new, ${existing.length} already exist`);
|
|
87
88
|
spinner.stop();
|
|
88
89
|
console.log();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for (const f of fresh) {
|
|
93
|
-
console.log(` ${f.rel}`);
|
|
94
|
-
}
|
|
95
|
-
console.log();
|
|
90
|
+
let toRestore;
|
|
91
|
+
if (options.yes) {
|
|
92
|
+
toRestore = fresh;
|
|
96
93
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
name: f.rel,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
else {
|
|
95
|
+
const choices = [];
|
|
96
|
+
if (fresh.length > 0) {
|
|
97
|
+
choices.push(new Separator(`── New files (${fresh.length}) ──`));
|
|
98
|
+
for (const f of fresh) {
|
|
99
|
+
choices.push({ name: f.rel, value: f, checked: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (existing.length > 0) {
|
|
103
|
+
choices.push(new Separator(`── Existing files — will overwrite (${existing.length}) ──`));
|
|
104
|
+
for (const f of existing) {
|
|
105
|
+
choices.push({ name: f.rel, value: f, checked: false });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
toRestore = await checkbox({
|
|
109
|
+
message: "Select files to restore:",
|
|
110
|
+
loop: false,
|
|
111
|
+
choices,
|
|
106
112
|
});
|
|
107
|
-
toRestore = [...fresh, ...selected];
|
|
108
113
|
console.log();
|
|
109
114
|
}
|
|
110
115
|
if (toRestore.length === 0) {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
interface FileChangeSummary {
|
|
2
|
+
created: string[];
|
|
3
|
+
deleted: string[];
|
|
4
|
+
modified: string[];
|
|
5
|
+
renamed: Array<{
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildCommitMessage(changes: FileChangeSummary, maxLen?: number): string;
|
|
11
|
+
export declare function gitPull(repository: string, commit?: string): Promise<void>;
|
|
12
|
+
export declare function gitLog(limit?: number): Promise<Array<{
|
|
13
|
+
hash: string;
|
|
14
|
+
date: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}>>;
|
|
2
17
|
export declare function gitCommitAndPush(): Promise<{
|
|
3
18
|
commitUrl: string | null;
|
|
4
19
|
} | null>;
|
|
20
|
+
export {};
|
package/dist/git.js
CHANGED
|
@@ -5,12 +5,50 @@ import { simpleGit, CleanOptions } from "simple-git";
|
|
|
5
5
|
import { logger } from "./log.js";
|
|
6
6
|
import { STAGING_DIR } from "./staging.js";
|
|
7
7
|
import { getCommitUrl } from "./commitUrl.js";
|
|
8
|
-
export
|
|
8
|
+
export function buildCommitMessage(changes, maxLen = 250) {
|
|
9
|
+
const unique = (paths) => [...new Set(paths.map((f) => path.basename(f)))];
|
|
10
|
+
const removed = unique(changes.deleted);
|
|
11
|
+
const added = unique(changes.created);
|
|
12
|
+
const modified = unique([...changes.modified, ...changes.renamed.map((r) => r.to)]);
|
|
13
|
+
const categories = [
|
|
14
|
+
{ label: "removed", files: removed },
|
|
15
|
+
{ label: "added", files: added },
|
|
16
|
+
{ label: "modified", files: modified },
|
|
17
|
+
].filter((c) => c.files.length > 0);
|
|
18
|
+
if (categories.length === 0) {
|
|
19
|
+
return "backup";
|
|
20
|
+
}
|
|
21
|
+
function format(cat, listFiles) {
|
|
22
|
+
if (listFiles) {
|
|
23
|
+
return `${cat.label}: ${cat.files.join(", ")}`;
|
|
24
|
+
}
|
|
25
|
+
const n = cat.files.length;
|
|
26
|
+
return `${cat.label}: ${n} file${n !== 1 ? "s" : ""}`;
|
|
27
|
+
}
|
|
28
|
+
function build(listFlags) {
|
|
29
|
+
return categories.map((cat, i) => format(cat, listFlags[i])).join("; ");
|
|
30
|
+
}
|
|
31
|
+
const flags = categories.map(() => true);
|
|
32
|
+
let msg = build(flags);
|
|
33
|
+
if (msg.length <= maxLen)
|
|
34
|
+
return msg;
|
|
35
|
+
for (const label of ["modified", "added", "removed"]) {
|
|
36
|
+
const idx = categories.findIndex((c) => c.label === label);
|
|
37
|
+
if (idx !== -1 && flags[idx]) {
|
|
38
|
+
flags[idx] = false;
|
|
39
|
+
msg = build(flags);
|
|
40
|
+
if (msg.length <= maxLen)
|
|
41
|
+
return msg;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return msg.slice(0, maxLen - 3) + "...";
|
|
45
|
+
}
|
|
46
|
+
export async function gitPull(repository, commit) {
|
|
9
47
|
if (fs.existsSync(path.join(STAGING_DIR, ".git"))) {
|
|
10
48
|
const git = simpleGit(STAGING_DIR);
|
|
11
49
|
await git.fetch("origin");
|
|
12
|
-
const
|
|
13
|
-
await git.reset(["--hard",
|
|
50
|
+
const target = commit ?? `origin/${await git.revparse(["--abbrev-ref", "HEAD"])}`;
|
|
51
|
+
await git.reset(["--hard", target]);
|
|
14
52
|
await git.clean(CleanOptions.FORCE, ["-d"]);
|
|
15
53
|
}
|
|
16
54
|
else {
|
|
@@ -23,9 +61,23 @@ export async function gitPull(repository) {
|
|
|
23
61
|
await git.init();
|
|
24
62
|
await git.addRemote("origin", repository);
|
|
25
63
|
}
|
|
64
|
+
if (commit) {
|
|
65
|
+
const git = simpleGit(STAGING_DIR);
|
|
66
|
+
await git.reset(["--hard", commit]);
|
|
67
|
+
await git.clean(CleanOptions.FORCE, ["-d"]);
|
|
68
|
+
}
|
|
26
69
|
}
|
|
27
70
|
logger.info("Synced staging directory from remote");
|
|
28
71
|
}
|
|
72
|
+
export async function gitLog(limit = 20) {
|
|
73
|
+
const git = simpleGit(STAGING_DIR);
|
|
74
|
+
const log = await git.log({ maxCount: limit });
|
|
75
|
+
return log.all.map((entry) => ({
|
|
76
|
+
hash: entry.hash,
|
|
77
|
+
date: entry.date,
|
|
78
|
+
message: entry.message,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
29
81
|
export async function gitCommitAndPush() {
|
|
30
82
|
const git = simpleGit(STAGING_DIR);
|
|
31
83
|
await git.add(".");
|
|
@@ -34,8 +86,7 @@ export async function gitCommitAndPush() {
|
|
|
34
86
|
logger.info("No changes to commit");
|
|
35
87
|
return null;
|
|
36
88
|
}
|
|
37
|
-
const
|
|
38
|
-
const message = `Automated backup: ${date}`;
|
|
89
|
+
const message = buildCommitMessage(status);
|
|
39
90
|
await git.commit(message);
|
|
40
91
|
await pRetry(async () => git.push(["-u", "origin", "HEAD"]), {
|
|
41
92
|
retries: 5,
|
|
@@ -43,11 +94,17 @@ export async function gitCommitAndPush() {
|
|
|
43
94
|
logger.info(`Push failed (attempt ${attemptNumber}, ${retriesLeft} retries left), rebasing`);
|
|
44
95
|
await git.fetch("origin");
|
|
45
96
|
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
46
|
-
|
|
97
|
+
try {
|
|
98
|
+
await git.rebase([`origin/${branch}`]);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
await git.rebase(["--abort"]);
|
|
102
|
+
throw new Error("Rebase conflict, aborting retry");
|
|
103
|
+
}
|
|
47
104
|
},
|
|
48
105
|
});
|
|
49
106
|
logger.info(`Committed and pushed: ${message}`);
|
|
50
|
-
const sha = await git.revparse(["HEAD"]);
|
|
107
|
+
const sha = (await git.revparse(["HEAD"])).trim();
|
|
51
108
|
const remoteUrl = (await git.remote(["get-url", "origin"])) ?? "";
|
|
52
109
|
const commitUrl = getCommitUrl(remoteUrl, sha);
|
|
53
110
|
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
|
|
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 {
|
|
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
|
|
7
|
-
const titleEscaped = title
|
|
9
|
+
const escaped = escapeAppleScript(message);
|
|
10
|
+
const titleEscaped = escapeAppleScript(title);
|
|
8
11
|
try {
|
|
9
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
if (
|
|
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(
|
|
8
|
+
return fg.sync(patterns, { absolute: true, dot: true, onlyFiles: true });
|
|
29
9
|
}
|
|
30
10
|
catch {
|
|
31
|
-
logger.warn(
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
modified: [],
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.6.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",
|
package/dist/restore.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function restore(repoUrl?: string): Promise<void>;
|
|
File without changes
|