backupman 0.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 +46 -0
- package/index.js +210 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# backupman
|
|
2
|
+
|
|
3
|
+
Ultra-simple local snapshot tool for when your code (or your AI) goes off the rails.
|
|
4
|
+
|
|
5
|
+
You run it inside your source folder (for example `src` or your project root), hit `save` when things look okay, and later you can `restore` any snapshot from an interactive list.
|
|
6
|
+
|
|
7
|
+
No branches, no remotes, no staging. Just: “this state feels safe → save”.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g backupman
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or inside a project:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install backupman
|
|
19
|
+
npm link
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
From the folder you want to protect (for example your project root or `src`):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
backupman save "before refactor"
|
|
28
|
+
backupman save "after AI edit"
|
|
29
|
+
|
|
30
|
+
backupman restore
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `backupman save "message"`
|
|
34
|
+
- Create a full snapshot of the current directory with a short message.
|
|
35
|
+
|
|
36
|
+
- `backupman restore`
|
|
37
|
+
- Show an interactive list of snapshots (newest first), auto-back up the current state, then restore the chosen snapshot.
|
|
38
|
+
|
|
39
|
+
## Notes
|
|
40
|
+
|
|
41
|
+
- Snapshots are stored under `.backupman/snapshots` in the same folder.
|
|
42
|
+
- On `save`, backupman copies all files and subfolders from the current directory.
|
|
43
|
+
- It ignores:
|
|
44
|
+
- `.backupman`
|
|
45
|
+
- `node_modules`
|
|
46
|
+
- `.git`
|
package/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const readline = require("readline");
|
|
5
|
+
|
|
6
|
+
class BackupMan {
|
|
7
|
+
constructor(rootDir) {
|
|
8
|
+
this.rootDir = rootDir;
|
|
9
|
+
this.backupDir = path.join(rootDir, ".backupman");
|
|
10
|
+
this.snapshotsDir = path.join(this.backupDir, "snapshots");
|
|
11
|
+
this.indexFile = path.join(this.backupDir, "index.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async ensureDirs() {
|
|
15
|
+
await fs.promises.mkdir(this.backupDir, { recursive: true });
|
|
16
|
+
await fs.promises.mkdir(this.snapshotsDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async loadIndex() {
|
|
20
|
+
try {
|
|
21
|
+
const data = await fs.promises.readFile(this.indexFile, "utf8");
|
|
22
|
+
const parsed = JSON.parse(data);
|
|
23
|
+
if (!parsed.snapshots || typeof parsed.lastId !== "number") {
|
|
24
|
+
throw new Error("Corrupted index");
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { lastId: 0, snapshots: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async saveIndex(index) {
|
|
33
|
+
const json = JSON.stringify(index, null, 2);
|
|
34
|
+
await fs.promises.writeFile(this.indexFile, json, "utf8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isIgnored(name) {
|
|
38
|
+
return name === ".backupman" || name === "node_modules" || name === ".git";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async copyDir(src, dest) {
|
|
42
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
43
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const name = entry.name;
|
|
46
|
+
if (this.isIgnored(name)) continue;
|
|
47
|
+
const srcPath = path.join(src, name);
|
|
48
|
+
const destPath = path.join(dest, name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
await this.copyDir(srcPath, destPath);
|
|
51
|
+
} else if (entry.isFile()) {
|
|
52
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async deleteEverythingExceptBackupDir() {
|
|
58
|
+
const entries = await fs.promises.readdir(this.rootDir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const name = entry.name;
|
|
61
|
+
if (name === ".backupman") continue;
|
|
62
|
+
const target = path.join(this.rootDir, name);
|
|
63
|
+
await fs.promises.rm(target, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async prompt(question) {
|
|
68
|
+
const rl = readline.createInterface({
|
|
69
|
+
input: process.stdin,
|
|
70
|
+
output: process.stdout
|
|
71
|
+
});
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
rl.question(question, answer => {
|
|
74
|
+
rl.close();
|
|
75
|
+
resolve(answer);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async createSnapshot(message) {
|
|
81
|
+
await this.ensureDirs();
|
|
82
|
+
const index = await this.loadIndex();
|
|
83
|
+
const id = index.lastId + 1;
|
|
84
|
+
const snapshotDir = path.join(this.snapshotsDir, String(id));
|
|
85
|
+
const now = new Date().toISOString();
|
|
86
|
+
|
|
87
|
+
await this.copyDir(this.rootDir, snapshotDir);
|
|
88
|
+
|
|
89
|
+
index.lastId = id;
|
|
90
|
+
index.snapshots.push({
|
|
91
|
+
id,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
message
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await this.saveIndex(index);
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
formatSnapshot(snapshot) {
|
|
101
|
+
const ts = snapshot.createdAt || "";
|
|
102
|
+
const msg = snapshot.message || "";
|
|
103
|
+
return `#${snapshot.id} [${ts}] ${msg}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async chooseSnapshot(index) {
|
|
107
|
+
if (!index.snapshots.length) {
|
|
108
|
+
console.log("No snapshots yet. Use `backupman save \"message\"` first.");
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const list = [...index.snapshots].sort((a, b) => b.id - a.id);
|
|
113
|
+
console.log("");
|
|
114
|
+
console.log("Available snapshots (newest first):");
|
|
115
|
+
list.forEach((s, i) => {
|
|
116
|
+
console.log(` [${i + 1}] ${this.formatSnapshot(s)}`);
|
|
117
|
+
});
|
|
118
|
+
console.log("");
|
|
119
|
+
|
|
120
|
+
while (true) {
|
|
121
|
+
const answer = (await this.prompt("Select snapshot number to restore (empty = cancel): ")).trim();
|
|
122
|
+
if (!answer) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const n = Number.parseInt(answer, 10);
|
|
126
|
+
if (!Number.isNaN(n) && n >= 1 && n <= list.length) {
|
|
127
|
+
return list[n - 1];
|
|
128
|
+
}
|
|
129
|
+
console.log("Invalid choice. Please enter a valid number or press Enter to cancel.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async restore() {
|
|
134
|
+
await this.ensureDirs();
|
|
135
|
+
const index = await this.loadIndex();
|
|
136
|
+
const snapshot = await this.chooseSnapshot(index);
|
|
137
|
+
if (!snapshot) {
|
|
138
|
+
console.log("Restore cancelled.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log("");
|
|
143
|
+
console.log(`You chose snapshot: ${this.formatSnapshot(snapshot)}`);
|
|
144
|
+
const confirm = (await this.prompt("Overwrite current files with this snapshot? (y/N): ")).trim().toLowerCase();
|
|
145
|
+
if (confirm !== "y" && confirm !== "yes") {
|
|
146
|
+
console.log("Restore aborted.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("Clearing current directory (except .backupman)...");
|
|
151
|
+
await this.deleteEverythingExceptBackupDir();
|
|
152
|
+
|
|
153
|
+
console.log("Restoring snapshot files...");
|
|
154
|
+
const srcSnapshotDir = path.join(this.snapshotsDir, String(snapshot.id));
|
|
155
|
+
await this.copyDir(srcSnapshotDir, this.rootDir);
|
|
156
|
+
|
|
157
|
+
console.log(`Done. Restored snapshot #${snapshot.id}.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function printUsage() {
|
|
162
|
+
console.log("backupman - ultra-simple local snapshot versioning");
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log("Usage:");
|
|
165
|
+
console.log(" backupman save \"message\" Save current directory as a snapshot");
|
|
166
|
+
console.log(" backupman restore Restore from a previous snapshot");
|
|
167
|
+
console.log("");
|
|
168
|
+
console.log("Notes:");
|
|
169
|
+
console.log(" - Snapshots are stored in .backupman/snapshots");
|
|
170
|
+
console.log(" - .backupman, node_modules, .git are ignored");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function main() {
|
|
174
|
+
const cwd = process.cwd();
|
|
175
|
+
const manager = new BackupMan(cwd);
|
|
176
|
+
const [, , command, ...args] = process.argv;
|
|
177
|
+
|
|
178
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
179
|
+
printUsage();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (command === "save") {
|
|
184
|
+
let message = args.join(" ").trim();
|
|
185
|
+
if (!message) {
|
|
186
|
+
message = await manager.prompt("Describe this snapshot (required): ");
|
|
187
|
+
if (!message || !message.trim()) {
|
|
188
|
+
console.error("Snapshot message is required.");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const id = await manager.createSnapshot(message.trim());
|
|
193
|
+
console.log(`Saved snapshot #${id}.`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (command === "restore") {
|
|
198
|
+
await manager.restore();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.error(`Unknown command: ${command}`);
|
|
203
|
+
printUsage();
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
main().catch(err => {
|
|
208
|
+
console.error("Error:", err && err.message ? err.message : err);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backupman",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ultra-simple local snapshot versioning tool for chaotic devs and mischievous AIs.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"backupman": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"backup",
|
|
14
|
+
"snapshot",
|
|
15
|
+
"versioning",
|
|
16
|
+
"cli",
|
|
17
|
+
"local"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {}
|
|
22
|
+
}
|