dorky 3.0.2 → 4.0.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/.dorky/history.json +22 -0
- package/README.md +25 -4
- package/bin/index.js +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "34aaf3c3",
|
|
4
|
+
"timestamp": "2026-04-22T18:30:13.819Z",
|
|
5
|
+
"files": {
|
|
6
|
+
".env": {
|
|
7
|
+
"mime-type": "application/octet-stream",
|
|
8
|
+
"hash": "ef690809a391f839b909e303e8d1f888"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "e5ecba71",
|
|
14
|
+
"timestamp": "2026-04-22T18:30:45.770Z",
|
|
15
|
+
"files": {
|
|
16
|
+
".env": {
|
|
17
|
+
"mime-type": "application/octet-stream",
|
|
18
|
+
"hash": "0f3124c0e7688e35ae2187da45f23f22"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
package/README.md
CHANGED
|
@@ -205,6 +205,23 @@ This command:
|
|
|
205
205
|
- Creates necessary directories
|
|
206
206
|
- Overwrites local files
|
|
207
207
|
|
|
208
|
+
### Show Push History (`-lg`)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
dorky --log
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Prints all past push commits in reverse chronological order, showing the commit ID, timestamp, and list of files included in each snapshot.
|
|
215
|
+
|
|
216
|
+
### Checkout a Commit (`-co`)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Restore files to a specific commit
|
|
220
|
+
dorky --checkout <commit-id>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Downloads the files as they were at the given commit from remote storage and restores the local staged/uploaded state to match. The commit ID can be found with `--log`. Prefix matching is supported (e.g. `dorky --checkout a1b2` if the full ID is `a1b2c3d4`).
|
|
224
|
+
|
|
208
225
|
### Destroy Project (`-d`)
|
|
209
226
|
|
|
210
227
|
```bash
|
|
@@ -241,7 +258,8 @@ After initialization:
|
|
|
241
258
|
your-project/
|
|
242
259
|
├── .dorky/
|
|
243
260
|
│ ├── credentials.json # Storage credentials (auto-ignored by git)
|
|
244
|
-
│
|
|
261
|
+
│ ├── metadata.json # Tracked files metadata
|
|
262
|
+
│ └── history.json # Push commit history
|
|
245
263
|
├── .dorkyignore # Exclusion patterns
|
|
246
264
|
└── .gitignore # Updated automatically
|
|
247
265
|
```
|
|
@@ -345,14 +363,17 @@ A graphical interface for dorky is available as a VS Code extension — manage s
|
|
|
345
363
|
- ✅ Recursive folder creation on pull
|
|
346
364
|
- ✅ Destroy project and clean up remote files
|
|
347
365
|
- ✅ Auto-recovery of AWS credentials from environment variables
|
|
366
|
+
- ✅ Push history with versioned remote snapshots
|
|
367
|
+
- ✅ Restore files to any previous push commit
|
|
348
368
|
|
|
349
369
|
## How It Works
|
|
350
370
|
|
|
351
|
-
1. **Initialization**: Creates `.dorky/` folder with metadata and
|
|
371
|
+
1. **Initialization**: Creates `.dorky/` folder with metadata, credentials, and history
|
|
352
372
|
2. **File Tracking**: Maintains a hash-based registry of files in `metadata.json`
|
|
353
373
|
3. **Smart Uploads**: Only uploads files that have changed (based on MD5 hash)
|
|
354
374
|
4. **Auto-detection**: Highlights `.env` and `.config` files during listing
|
|
355
375
|
5. **Security**: Automatically updates `.gitignore` to protect credentials
|
|
376
|
+
6. **History**: Each push saves a commit entry in `history.json` and uploads a versioned snapshot to `<project>/.dorky-history/<commit-id>/` on remote storage, enabling point-in-time restore via `--checkout`
|
|
356
377
|
|
|
357
378
|
## Security Best Practices
|
|
358
379
|
|
|
@@ -415,10 +436,10 @@ ISC License - see [LICENSE](LICENSE) file for details.
|
|
|
415
436
|
- [x] Extension for VS Code to list and highlight them like git (Major release)
|
|
416
437
|
- [ ] MCP server (Minor release)
|
|
417
438
|
- [ ] Encryption of files (Minor release)
|
|
418
|
-
- [
|
|
439
|
+
- [x] Add stages for variables (Major release)
|
|
419
440
|
- [ ] Migrate dorky project to another storage (partially implemented)
|
|
420
441
|
- [ ] Add more test cases
|
|
421
442
|
- [ ] Deletion of files
|
|
422
443
|
- [ ] Edge cases for failure when credentials are invalid
|
|
423
|
-
- [
|
|
444
|
+
- [x] Add coverage reports badges
|
|
424
445
|
|
package/bin/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const { google } = require('googleapis');
|
|
|
16
16
|
const DORKY_DIR = ".dorky";
|
|
17
17
|
const METADATA_PATH = path.join(DORKY_DIR, "metadata.json");
|
|
18
18
|
const CREDENTIALS_PATH = path.join(DORKY_DIR, "credentials.json");
|
|
19
|
+
const HISTORY_PATH = path.join(DORKY_DIR, "history.json");
|
|
19
20
|
const GD_CREDENTIALS_PATH = path.join(__dirname, "../google-drive-credentials.json");
|
|
20
21
|
const SCOPES = ['https://www.googleapis.com/auth/drive'];
|
|
21
22
|
|
|
@@ -50,6 +51,8 @@ const args = yargs
|
|
|
50
51
|
.option("pull", { alias: "pl", describe: "Pull files", type: "string" })
|
|
51
52
|
.option("migrate", { alias: "m", describe: "Migrate project", type: "string" })
|
|
52
53
|
.option("destroy", { alias: "d", describe: "Destroy project", type: "boolean" })
|
|
54
|
+
.option("log", { alias: "lg", describe: "Show push history", type: "boolean" })
|
|
55
|
+
.option("checkout", { alias: "co", describe: "Restore files from a history commit", type: "string" })
|
|
53
56
|
.help('help').strict().argv;
|
|
54
57
|
|
|
55
58
|
if (Object.keys(args).length === 2 && args._.length === 0) yargs.showHelp();
|
|
@@ -110,6 +113,7 @@ async function init(storage) {
|
|
|
110
113
|
|
|
111
114
|
mkdirSync(DORKY_DIR);
|
|
112
115
|
writeJson(METADATA_PATH, { "stage-1-files": {}, "uploaded-files": {} });
|
|
116
|
+
writeJson(HISTORY_PATH, []);
|
|
113
117
|
writeFileSync(".dorkyignore", "");
|
|
114
118
|
writeJson(CREDENTIALS_PATH, credentials);
|
|
115
119
|
console.log(chalk.green("✔ Dorky project initialized successfully."));
|
|
@@ -326,6 +330,36 @@ async function push() {
|
|
|
326
330
|
|
|
327
331
|
meta["uploaded-files"] = { ...meta["stage-1-files"] };
|
|
328
332
|
writeJson(METADATA_PATH, meta);
|
|
333
|
+
|
|
334
|
+
const commitFiles = { ...meta["stage-1-files"] };
|
|
335
|
+
const commitId = md5(JSON.stringify(commitFiles)).slice(0, 8);
|
|
336
|
+
const history = existsSync(HISTORY_PATH) ? JSON.parse(readFileSync(HISTORY_PATH)) : [];
|
|
337
|
+
if (!history.find(e => e.id === commitId)) {
|
|
338
|
+
history.push({ id: commitId, timestamp: new Date().toISOString(), files: commitFiles });
|
|
339
|
+
writeJson(HISTORY_PATH, history);
|
|
340
|
+
|
|
341
|
+
const root = path.basename(process.cwd());
|
|
342
|
+
const historyPrefix = path.join(root, ".dorky-history", commitId);
|
|
343
|
+
if (creds.storage === "aws") {
|
|
344
|
+
await runS3(creds, async (s3, bucket) => {
|
|
345
|
+
await Promise.all(Object.keys(commitFiles).map(async f => {
|
|
346
|
+
const key = path.join(historyPrefix, f);
|
|
347
|
+
await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: readFileSync(f) }));
|
|
348
|
+
}));
|
|
349
|
+
});
|
|
350
|
+
} else if (creds.storage === "google-drive") {
|
|
351
|
+
await runDrive(async (drive) => {
|
|
352
|
+
for (const f of Object.keys(commitFiles)) {
|
|
353
|
+
const parentId = await getFolderId(path.join(root, ".dorky-history", commitId, path.dirname(f)), drive);
|
|
354
|
+
await drive.files.create({
|
|
355
|
+
requestBody: { name: path.basename(f), parents: [parentId] },
|
|
356
|
+
media: { mimeType: commitFiles[f]["mime-type"], body: createReadStream(f) }
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
console.log(chalk.cyan(`ℹ History commit saved: ${commitId}`));
|
|
362
|
+
}
|
|
329
363
|
}
|
|
330
364
|
|
|
331
365
|
async function pull() {
|
|
@@ -361,6 +395,71 @@ async function pull() {
|
|
|
361
395
|
}
|
|
362
396
|
}
|
|
363
397
|
|
|
398
|
+
function log() {
|
|
399
|
+
checkDorkyProject();
|
|
400
|
+
const history = existsSync(HISTORY_PATH) ? JSON.parse(readFileSync(HISTORY_PATH)) : [];
|
|
401
|
+
if (!history.length) return console.log(chalk.yellow("ℹ No history found. Push some files first."));
|
|
402
|
+
console.log(chalk.blue.bold("\n📜 Push History:\n"));
|
|
403
|
+
[...history].reverse().forEach((entry, i) => {
|
|
404
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
405
|
+
const fileCount = Object.keys(entry.files).length;
|
|
406
|
+
console.log(chalk.yellow(` commit ${entry.id}`) + (i === 0 ? chalk.green(" (latest)") : ""));
|
|
407
|
+
console.log(chalk.gray(` Date: ${date}`));
|
|
408
|
+
console.log(chalk.gray(` Files: ${fileCount}`));
|
|
409
|
+
Object.keys(entry.files).forEach(f => console.log(chalk.cyan(` • ${f}`)));
|
|
410
|
+
console.log();
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function checkout(commitId) {
|
|
415
|
+
checkDorkyProject();
|
|
416
|
+
if (!await checkCredentials()) return;
|
|
417
|
+
|
|
418
|
+
const history = existsSync(HISTORY_PATH) ? JSON.parse(readFileSync(HISTORY_PATH)) : [];
|
|
419
|
+
const entry = history.find(e => e.id === commitId || e.id.startsWith(commitId));
|
|
420
|
+
if (!entry) return console.log(chalk.red(`✖ Commit not found: ${commitId}. Run --log to see available commits.`));
|
|
421
|
+
|
|
422
|
+
console.log(chalk.blue.bold(`\n⏪ Checking out commit ${entry.id} (${new Date(entry.timestamp).toLocaleString()}):\n`));
|
|
423
|
+
|
|
424
|
+
const creds = readJson(CREDENTIALS_PATH);
|
|
425
|
+
const root = path.basename(process.cwd());
|
|
426
|
+
const historyPrefix = path.join(root, ".dorky-history", entry.id);
|
|
427
|
+
|
|
428
|
+
if (creds.storage === "aws") {
|
|
429
|
+
await runS3(creds, async (s3, bucket) => {
|
|
430
|
+
await Promise.all(Object.keys(entry.files).map(async f => {
|
|
431
|
+
const key = path.join(historyPrefix, f);
|
|
432
|
+
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
433
|
+
if (!existsSync(path.dirname(f))) mkdirSync(path.dirname(f), { recursive: true });
|
|
434
|
+
writeFileSync(f, await Body.transformToString());
|
|
435
|
+
console.log(chalk.green(`✔ Restored: ${f}`));
|
|
436
|
+
}));
|
|
437
|
+
});
|
|
438
|
+
} else if (creds.storage === "google-drive") {
|
|
439
|
+
await runDrive(async (drive) => {
|
|
440
|
+
for (const f of Object.keys(entry.files)) {
|
|
441
|
+
const parentId = await getFolderId(path.join(root, ".dorky-history", entry.id, path.dirname(f)), drive, false);
|
|
442
|
+
if (!parentId) { console.log(chalk.red(`✖ Remote history folder missing for: ${f}`)); continue; }
|
|
443
|
+
const res = await drive.files.list({
|
|
444
|
+
q: `name='${path.basename(f)}' and '${parentId}' in parents and trashed=false`,
|
|
445
|
+
fields: 'files(id)'
|
|
446
|
+
});
|
|
447
|
+
if (!res.data.files[0]) { console.log(chalk.red(`✖ Missing remote history file: ${f}`)); continue; }
|
|
448
|
+
const data = await drive.files.get({ fileId: res.data.files[0].id, alt: 'media' });
|
|
449
|
+
if (!existsSync(path.dirname(f))) mkdirSync(path.dirname(f), { recursive: true });
|
|
450
|
+
writeFileSync(f, await data.data.text());
|
|
451
|
+
console.log(chalk.green(`✔ Restored: ${f}`));
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const meta = readJson(METADATA_PATH);
|
|
457
|
+
meta["stage-1-files"] = { ...entry.files };
|
|
458
|
+
meta["uploaded-files"] = { ...entry.files };
|
|
459
|
+
writeJson(METADATA_PATH, meta);
|
|
460
|
+
console.log(chalk.cyan(`\nℹ Staged and uploaded state restored to commit ${entry.id}.`));
|
|
461
|
+
}
|
|
462
|
+
|
|
364
463
|
async function destroy() {
|
|
365
464
|
checkDorkyProject();
|
|
366
465
|
if (!await checkCredentials()) return;
|
|
@@ -403,4 +502,6 @@ if (args.add !== undefined) add(args.add);
|
|
|
403
502
|
if (args.rm !== undefined) rm(args.rm);
|
|
404
503
|
if (args.push !== undefined) push();
|
|
405
504
|
if (args.pull !== undefined) pull();
|
|
505
|
+
if (args.log !== undefined) log();
|
|
506
|
+
if (args.checkout !== undefined) checkout(args.checkout);
|
|
406
507
|
if (args.destroy !== undefined) destroy();
|