backdot 1.0.0 → 1.1.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 +19 -62
- package/dist/cli.js +55 -17
- package/dist/config.d.ts +4 -0
- package/dist/config.js +12 -2
- package/dist/git.d.ts +1 -1
- package/dist/git.js +12 -20
- package/dist/resolve.js +8 -1
- package/dist/restore.js +24 -3
- package/dist/staging.d.ts +4 -1
- package/dist/staging.js +38 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,89 +1,46 @@
|
|
|
1
1
|
# backdot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Back up dotfiles and gitignored files to a private Git repo — on demand or on a daily schedule.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
1. Reads `~/.backdot.json` for the target repository and file entries
|
|
8
|
-
2. Resolves file entries — either gitignored files in a directory or glob patterns
|
|
9
|
-
3. Copies resolved files to `~/.backdot/repo/`, preserving directory structure relative to `~`
|
|
10
|
-
4. Commits and pushes changes to the configured remote
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
5
|
+
## Getting started
|
|
13
6
|
|
|
14
7
|
```bash
|
|
15
8
|
npm install -g backdot
|
|
16
9
|
```
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Configuration
|
|
21
|
-
|
|
22
|
-
Create `~/.backdot.json`:
|
|
11
|
+
Create `~/.backdot.json` pointing to a private repo and the files you want backed up:
|
|
23
12
|
|
|
24
13
|
```json
|
|
25
14
|
{
|
|
26
15
|
"repository": "git@github.com:USERNAME/dotfiles-backup.git",
|
|
16
|
+
"machine": "my-work-laptop",
|
|
27
17
|
"files.gitignored": ["~/my-project"],
|
|
28
|
-
"files.match": ["~/.zshrc", "~/.config/
|
|
18
|
+
"files.match": ["~/.zshrc", "~/.oh-my-zsh/custom/*.zsh", "~/.ssh/config/config", "~/.npmrc"]
|
|
29
19
|
}
|
|
30
20
|
```
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| Key | Description |
|
|
35
|
-
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
-
| `files.gitignored` | Array of directories. Backs up all gitignored files in each directory (runs `git ls-files --others --ignored --exclude-standard`) |
|
|
37
|
-
| `files.match` | Array of glob patterns. Backs up all matching files (powered by [fast-glob](https://github.com/mrmlnc/fast-glob)) |
|
|
38
|
-
|
|
39
|
-
Both keys are optional, but at least one must be present and non-empty. Paths support `~` expansion.
|
|
40
|
-
|
|
41
|
-
## Usage
|
|
42
|
-
|
|
43
|
-
### Run a backup
|
|
22
|
+
Run your first backup:
|
|
44
23
|
|
|
45
24
|
```bash
|
|
46
25
|
backdot --backup
|
|
47
26
|
```
|
|
48
27
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
backdot --restore
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Pulls the latest state from the remote backup repo and copies files back to their original locations. If any files already exist, you'll be prompted to select which ones to overwrite.
|
|
56
|
-
|
|
57
|
-
### Schedule daily backups (macOS)
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
backdot --schedule
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Installs a launchd job (`com.backdot.daemon`) that runs the backup daily at 02:00.
|
|
64
|
-
|
|
65
|
-
### Remove the schedule
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
backdot --unschedule
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Check status
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
backdot --status
|
|
75
|
-
```
|
|
28
|
+
## Configuration
|
|
76
29
|
|
|
77
|
-
|
|
30
|
+
| Key | Description |
|
|
31
|
+
| ------------------ | ------------------------------------------------------ |
|
|
32
|
+
| `files.gitignored` | Directories to scan for gitignored files |
|
|
33
|
+
| `files.match` | Glob patterns matching individual files or directories |
|
|
78
34
|
|
|
79
|
-
##
|
|
35
|
+
## Commands
|
|
80
36
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
37
|
+
| Command | Description |
|
|
38
|
+
| -------------- | ------------------------------------------------------ |
|
|
39
|
+
| `--backup` | Run a backup now |
|
|
40
|
+
| `--restore` | Restore files to their original locations |
|
|
41
|
+
| `--schedule` | Schedule automatic daily backup via launchd (Mac-only) |
|
|
42
|
+
| `--unschedule` | Remove the daily schedule |
|
|
43
|
+
| `--status` | Show schedule and resolved file list |
|
|
87
44
|
|
|
88
45
|
## Development
|
|
89
46
|
|
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import ora from "ora";
|
|
3
|
-
import { loadConfig } from "./config.js";
|
|
5
|
+
import { loadConfig, CONFIG_PATH } from "./config.js";
|
|
4
6
|
import { resolveFiles } from "./resolve.js";
|
|
5
|
-
import { copyToStaging } from "./staging.js";
|
|
6
|
-
import {
|
|
7
|
+
import { cleanStaging, copyToStaging, writeRepoReadme } from "./staging.js";
|
|
8
|
+
import { gitPull, gitCommitAndPush } from "./git.js";
|
|
7
9
|
import { restore } from "./restore.js";
|
|
8
10
|
import { setupLaunchd, uninstallLaunchd, isScheduled } from "./plist.js";
|
|
9
11
|
import { logger } from "./log.js";
|
|
12
|
+
function getVersion() {
|
|
13
|
+
const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json");
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
return pkg.version ?? "unknown";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
async function backup() {
|
|
11
23
|
logger.info("Starting backup");
|
|
12
24
|
const config = loadConfig();
|
|
13
25
|
logger.info(`Repository: ${config.repository}`);
|
|
26
|
+
logger.info(`Machine: ${config.machine}`);
|
|
14
27
|
const spinner = ora("Resolving files").start();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
try {
|
|
29
|
+
const userFiles = resolveFiles(config.files);
|
|
30
|
+
logger.info(`Resolved ${userFiles.length} file(s)`);
|
|
31
|
+
if (userFiles.length === 0) {
|
|
32
|
+
spinner.info("No files resolved, nothing to back up");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const files = [...userFiles, CONFIG_PATH];
|
|
36
|
+
spinner.text = "Syncing with remote";
|
|
37
|
+
await gitPull(config.repository);
|
|
38
|
+
spinner.text = `Copying ${files.length} file(s) to staging`;
|
|
39
|
+
cleanStaging(config.machine);
|
|
40
|
+
copyToStaging(files, config.machine);
|
|
41
|
+
writeRepoReadme();
|
|
42
|
+
spinner.text = "Pushing to remote";
|
|
43
|
+
await gitCommitAndPush();
|
|
44
|
+
spinner.succeed("Backup complete");
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
spinner.fail("Backup failed");
|
|
49
|
+
throw err;
|
|
20
50
|
}
|
|
21
|
-
spinner.text = `Copying ${files.length} file(s) to staging`;
|
|
22
|
-
copyToStaging(files);
|
|
23
|
-
spinner.text = "Pushing to remote";
|
|
24
|
-
await gitSync(config.repository);
|
|
25
|
-
spinner.succeed("Backup complete");
|
|
26
|
-
console.log();
|
|
27
51
|
logger.info("Backup complete");
|
|
28
52
|
}
|
|
29
53
|
function status() {
|
|
@@ -33,14 +57,17 @@ function status() {
|
|
|
33
57
|
try {
|
|
34
58
|
const config = loadConfig();
|
|
35
59
|
console.log(` Repo: ${config.repository}`);
|
|
60
|
+
console.log(` Machine: ${config.machine}`);
|
|
36
61
|
console.log();
|
|
37
62
|
const spinner = ora("Resolving files").start();
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
63
|
+
const userFiles = resolveFiles(config.files);
|
|
64
|
+
if (userFiles.length === 0) {
|
|
40
65
|
spinner.warn("No files resolved. Check your ~/.backdot.json entries.");
|
|
41
66
|
}
|
|
42
67
|
else {
|
|
43
|
-
|
|
68
|
+
const files = [...userFiles, CONFIG_PATH];
|
|
69
|
+
spinner.stop();
|
|
70
|
+
console.log(`${files.length} file(s) resolved:`);
|
|
44
71
|
console.log();
|
|
45
72
|
for (const file of files) {
|
|
46
73
|
console.log(` ${file}`);
|
|
@@ -54,6 +81,11 @@ function status() {
|
|
|
54
81
|
}
|
|
55
82
|
console.log();
|
|
56
83
|
}
|
|
84
|
+
function requireMacOS() {
|
|
85
|
+
if (process.platform !== "darwin") {
|
|
86
|
+
throw new Error("Scheduling is only supported on macOS (launchd). Use cron or systemd on Linux.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
57
89
|
async function main() {
|
|
58
90
|
const args = process.argv.slice(2);
|
|
59
91
|
const command = args[0];
|
|
@@ -62,9 +94,11 @@ async function main() {
|
|
|
62
94
|
await backup();
|
|
63
95
|
}
|
|
64
96
|
else if (command === "--schedule") {
|
|
97
|
+
requireMacOS();
|
|
65
98
|
setupLaunchd();
|
|
66
99
|
}
|
|
67
100
|
else if (command === "--unschedule") {
|
|
101
|
+
requireMacOS();
|
|
68
102
|
uninstallLaunchd();
|
|
69
103
|
}
|
|
70
104
|
else if (command === "--status") {
|
|
@@ -73,6 +107,9 @@ async function main() {
|
|
|
73
107
|
else if (command === "--restore") {
|
|
74
108
|
await restore();
|
|
75
109
|
}
|
|
110
|
+
else if (command === "--version") {
|
|
111
|
+
console.log(getVersion());
|
|
112
|
+
}
|
|
76
113
|
else {
|
|
77
114
|
console.log();
|
|
78
115
|
console.log(" Usage: backdot <command>");
|
|
@@ -84,6 +121,7 @@ async function main() {
|
|
|
84
121
|
console.log(" --schedule Install daily backup schedule (macOS launchd)");
|
|
85
122
|
console.log(" --unschedule Remove the daily backup schedule");
|
|
86
123
|
console.log(" --status Show schedule and resolved files");
|
|
124
|
+
console.log(" --version Show version");
|
|
87
125
|
console.log();
|
|
88
126
|
if (command && command !== "--help") {
|
|
89
127
|
console.error(` Unknown command: ${command}`);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export declare const CONFIG_PATH: string;
|
|
2
3
|
export declare function expandTilde(p: string): string;
|
|
3
4
|
declare const ConfigSchema: z.ZodPipe<z.ZodObject<{
|
|
4
5
|
repository: z.ZodString;
|
|
6
|
+
machine: z.ZodString;
|
|
5
7
|
"files.gitignored": z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>>;
|
|
6
8
|
"files.match": z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>>;
|
|
7
9
|
}, z.core.$strip>, z.ZodTransform<{
|
|
8
10
|
repository: string;
|
|
11
|
+
machine: string;
|
|
9
12
|
files: {
|
|
10
13
|
gitignored: string[];
|
|
11
14
|
match: string[];
|
|
12
15
|
};
|
|
13
16
|
}, {
|
|
14
17
|
repository: string;
|
|
18
|
+
machine: string;
|
|
15
19
|
"files.gitignored": string[];
|
|
16
20
|
"files.match": string[];
|
|
17
21
|
}>>;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
const CONFIG_PATH = path.join(os.homedir(), ".backdot.json");
|
|
5
|
+
export const CONFIG_PATH = path.join(os.homedir(), ".backdot.json");
|
|
6
6
|
export function expandTilde(p) {
|
|
7
7
|
if (p.startsWith("~/") || p === "~") {
|
|
8
8
|
return path.join(os.homedir(), p.slice(1));
|
|
@@ -13,6 +13,7 @@ const pathList = z.array(z.string().min(1).transform(expandTilde)).optional().de
|
|
|
13
13
|
const ConfigSchema = z
|
|
14
14
|
.object({
|
|
15
15
|
repository: z.string().min(1),
|
|
16
|
+
machine: z.string().min(1),
|
|
16
17
|
"files.gitignored": pathList,
|
|
17
18
|
"files.match": pathList,
|
|
18
19
|
})
|
|
@@ -21,6 +22,7 @@ const ConfigSchema = z
|
|
|
21
22
|
})
|
|
22
23
|
.transform((c) => ({
|
|
23
24
|
repository: c.repository,
|
|
25
|
+
machine: c.machine,
|
|
24
26
|
files: {
|
|
25
27
|
gitignored: c["files.gitignored"],
|
|
26
28
|
match: c["files.match"],
|
|
@@ -44,5 +46,13 @@ export function loadConfig() {
|
|
|
44
46
|
catch {
|
|
45
47
|
throw new Error(`Invalid JSON in config file: ${CONFIG_PATH}`);
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
const result = ConfigSchema.safeParse(parsed);
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
const messages = result.error.issues.map((i) => {
|
|
52
|
+
const path = i.path.length > 0 ? `"${i.path.join(".")}"` : "config";
|
|
53
|
+
return ` - ${path}: ${i.message}`;
|
|
54
|
+
});
|
|
55
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}:\n${messages.join("\n")}`);
|
|
56
|
+
}
|
|
57
|
+
return result.data;
|
|
48
58
|
}
|
package/dist/git.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function gitPull(repository: string): Promise<void>;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function gitCommitAndPush(): Promise<void>;
|
package/dist/git.js
CHANGED
|
@@ -12,20 +12,20 @@ export async function gitPull(repository) {
|
|
|
12
12
|
await git.clean(CleanOptions.FORCE, ["-d"]);
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
await simpleGit().clone(repository, STAGING_DIR);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
fs.mkdirSync(STAGING_DIR, { recursive: true });
|
|
20
|
+
const git = simpleGit(STAGING_DIR);
|
|
21
|
+
await git.init();
|
|
22
|
+
await git.addRemote("origin", repository);
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
logger.info("Synced staging directory from remote");
|
|
18
26
|
}
|
|
19
|
-
export async function
|
|
20
|
-
if (!fs.existsSync(STAGING_DIR)) {
|
|
21
|
-
fs.mkdirSync(STAGING_DIR, { recursive: true });
|
|
22
|
-
}
|
|
27
|
+
export async function gitCommitAndPush() {
|
|
23
28
|
const git = simpleGit(STAGING_DIR);
|
|
24
|
-
if (!fs.existsSync(path.join(STAGING_DIR, ".git"))) {
|
|
25
|
-
logger.info("Initializing new git repository in staging directory");
|
|
26
|
-
await git.init();
|
|
27
|
-
await git.addRemote("origin", repository);
|
|
28
|
-
}
|
|
29
29
|
await git.add(".");
|
|
30
30
|
const status = await git.status();
|
|
31
31
|
if (status.isClean()) {
|
|
@@ -35,14 +35,6 @@ export async function gitSync(repository) {
|
|
|
35
35
|
const date = new Date().toISOString().split("T")[0];
|
|
36
36
|
const message = `Automated backup: ${date}`;
|
|
37
37
|
await git.commit(message);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await git.push(["-u", "origin", "HEAD"]);
|
|
41
|
-
logger.info("Pushed to remote");
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
-
logger.error(`Push failed: ${msg}`);
|
|
46
|
-
throw new Error(`Push failed: ${msg}`);
|
|
47
|
-
}
|
|
38
|
+
await git.push(["-u", "origin", "HEAD"]);
|
|
39
|
+
logger.info(`Committed and pushed: ${message}`);
|
|
48
40
|
}
|
package/dist/resolve.js
CHANGED
|
@@ -32,6 +32,7 @@ function resolveGlob(pattern) {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10 MB
|
|
35
36
|
/**
|
|
36
37
|
* Resolve all file entries to absolute paths.
|
|
37
38
|
* Skips entries that fail resolution and logs warnings.
|
|
@@ -44,7 +45,8 @@ export function resolveFiles(files) {
|
|
|
44
45
|
for (const pattern of files.match) {
|
|
45
46
|
allFiles.push(...resolveGlob(pattern));
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
const unique = [...new Set(allFiles)];
|
|
49
|
+
return unique.filter((filePath) => {
|
|
48
50
|
try {
|
|
49
51
|
fs.accessSync(filePath, fs.constants.R_OK);
|
|
50
52
|
const stat = fs.statSync(filePath);
|
|
@@ -52,6 +54,11 @@ export function resolveFiles(files) {
|
|
|
52
54
|
logger.warn(`Not a regular file, skipping: ${filePath}`);
|
|
53
55
|
return false;
|
|
54
56
|
}
|
|
57
|
+
if (stat.size > LARGE_FILE_THRESHOLD) {
|
|
58
|
+
const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
|
|
59
|
+
logger.warn(`Large file (${sizeMB} MB), skipping: ${filePath}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
55
62
|
return true;
|
|
56
63
|
}
|
|
57
64
|
catch {
|
package/dist/restore.js
CHANGED
|
@@ -5,7 +5,7 @@ import ora from "ora";
|
|
|
5
5
|
import { checkbox } from "@inquirer/prompts";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
7
|
import { gitPull } from "./git.js";
|
|
8
|
-
import { STAGING_DIR } from "./staging.js";
|
|
8
|
+
import { STAGING_DIR, machineDir } from "./staging.js";
|
|
9
9
|
import { logger } from "./log.js";
|
|
10
10
|
const HOME = os.homedir();
|
|
11
11
|
function walkDir(dir) {
|
|
@@ -23,13 +23,34 @@ function walkDir(dir) {
|
|
|
23
23
|
}
|
|
24
24
|
return results;
|
|
25
25
|
}
|
|
26
|
+
function listMachines() {
|
|
27
|
+
if (!fs.existsSync(STAGING_DIR))
|
|
28
|
+
return [];
|
|
29
|
+
return fs
|
|
30
|
+
.readdirSync(STAGING_DIR, { withFileTypes: true })
|
|
31
|
+
.filter((e) => e.isDirectory() && e.name !== ".git")
|
|
32
|
+
.map((e) => e.name);
|
|
33
|
+
}
|
|
26
34
|
export async function restore() {
|
|
27
35
|
logger.info("Starting restore");
|
|
28
36
|
const config = loadConfig();
|
|
37
|
+
const baseDir = machineDir(config.machine);
|
|
29
38
|
const spinner = ora("Fetching latest backup").start();
|
|
30
39
|
await gitPull(config.repository);
|
|
31
40
|
spinner.text = "Resolving files";
|
|
32
|
-
|
|
41
|
+
if (!fs.existsSync(baseDir)) {
|
|
42
|
+
spinner.stop();
|
|
43
|
+
const available = listMachines();
|
|
44
|
+
if (available.length > 0) {
|
|
45
|
+
console.log(`\n No backup found for machine "${config.machine}".`);
|
|
46
|
+
console.log(` Available machines: ${available.join(", ")}\n`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`\n No backup found for machine "${config.machine}". The repository is empty.\n`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const stagedFiles = walkDir(baseDir);
|
|
33
54
|
logger.info(`Found ${stagedFiles.length} file(s) in backup repository`);
|
|
34
55
|
if (stagedFiles.length === 0) {
|
|
35
56
|
spinner.stop();
|
|
@@ -37,7 +58,7 @@ export async function restore() {
|
|
|
37
58
|
return;
|
|
38
59
|
}
|
|
39
60
|
const fileMappings = stagedFiles.map((src) => {
|
|
40
|
-
const rel = path.relative(
|
|
61
|
+
const rel = path.relative(baseDir, src);
|
|
41
62
|
return { src, dest: path.join(HOME, rel), rel };
|
|
42
63
|
});
|
|
43
64
|
const existing = fileMappings.filter((f) => fs.existsSync(f.dest));
|
package/dist/staging.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export declare const STAGING_DIR: string;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function machineDir(machine: string): string;
|
|
3
|
+
export declare function cleanStaging(machine: string): void;
|
|
4
|
+
export declare function copyToStaging(files: string[], machine: string): void;
|
|
5
|
+
export declare function writeRepoReadme(): void;
|
package/dist/staging.js
CHANGED
|
@@ -4,15 +4,24 @@ import os from "node:os";
|
|
|
4
4
|
import { logger } from "./log.js";
|
|
5
5
|
const HOME = os.homedir();
|
|
6
6
|
export const STAGING_DIR = path.join(HOME, ".backdot", "repo");
|
|
7
|
-
export function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function machineDir(machine) {
|
|
8
|
+
return path.join(STAGING_DIR, machine);
|
|
9
|
+
}
|
|
10
|
+
export function cleanStaging(machine) {
|
|
11
|
+
const dir = machineDir(machine);
|
|
12
|
+
if (!fs.existsSync(dir))
|
|
13
|
+
return;
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
logger.info(`Cleaned staging directory for machine "${machine}"`);
|
|
16
|
+
}
|
|
17
|
+
export function copyToStaging(files, machine) {
|
|
18
|
+
const dir = machineDir(machine);
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
20
|
let copied = 0;
|
|
12
21
|
for (const filePath of files) {
|
|
13
22
|
const rel = path.relative(HOME, filePath);
|
|
14
23
|
const destRel = rel.startsWith("..") ? filePath.slice(1) : rel;
|
|
15
|
-
const dest = path.join(
|
|
24
|
+
const dest = path.join(dir, destRel);
|
|
16
25
|
try {
|
|
17
26
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
18
27
|
fs.copyFileSync(filePath, dest);
|
|
@@ -24,3 +33,27 @@ export function copyToStaging(files) {
|
|
|
24
33
|
}
|
|
25
34
|
logger.info(`Copied ${copied} file(s) to staging`);
|
|
26
35
|
}
|
|
36
|
+
const REPO_README = `# Dotfiles Backup
|
|
37
|
+
|
|
38
|
+
This repository contains dotfiles backed up automatically using [backdot](https://github.com/sorenlouv/backdot).
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Install backdot:
|
|
43
|
+
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
npm install -g backdot
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
Restore files from this backup:
|
|
49
|
+
|
|
50
|
+
\`\`\`bash
|
|
51
|
+
backdot --restore
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
For full documentation, configuration options, and scheduling, see the [official README](https://github.com/sorenlouv/backdot).
|
|
55
|
+
`;
|
|
56
|
+
export function writeRepoReadme() {
|
|
57
|
+
fs.writeFileSync(path.join(STAGING_DIR, "README.md"), REPO_README);
|
|
58
|
+
logger.info("Wrote README.md to staging directory");
|
|
59
|
+
}
|