dorky 3.0.1 → 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.
@@ -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
- └── metadata.json # Tracked files metadata
261
+ ├── metadata.json # Tracked files metadata
262
+ │ └── history.json # Push commit history
245
263
  ├── .dorkyignore # Exclusion patterns
246
264
  └── .gitignore # Updated automatically
247
265
  ```
@@ -323,6 +341,13 @@ dorky --push
323
341
  dorky --pull
324
342
  ```
325
343
 
344
+ ## VS Code Extension
345
+
346
+ A graphical interface for dorky is available as a VS Code extension — manage staged and uploaded files directly from the sidebar without leaving your editor.
347
+
348
+ - 📦 [dorky-extension on VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=trishantpahwa.dorky-extension)
349
+ - 🐙 [Extension source](https://github.com/trishantpahwa/dorky/tree/main/extension/dorky-extension)
350
+
326
351
  ## Features
327
352
 
328
353
  - ✅ AWS S3 storage integration
@@ -338,14 +363,17 @@ dorky --pull
338
363
  - ✅ Recursive folder creation on pull
339
364
  - ✅ Destroy project and clean up remote files
340
365
  - ✅ Auto-recovery of AWS credentials from environment variables
366
+ - ✅ Push history with versioned remote snapshots
367
+ - ✅ Restore files to any previous push commit
341
368
 
342
369
  ## How It Works
343
370
 
344
- 1. **Initialization**: Creates `.dorky/` folder with metadata and credentials
371
+ 1. **Initialization**: Creates `.dorky/` folder with metadata, credentials, and history
345
372
  2. **File Tracking**: Maintains a hash-based registry of files in `metadata.json`
346
373
  3. **Smart Uploads**: Only uploads files that have changed (based on MD5 hash)
347
374
  4. **Auto-detection**: Highlights `.env` and `.config` files during listing
348
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`
349
377
 
350
378
  ## Security Best Practices
351
379
 
@@ -408,10 +436,10 @@ ISC License - see [LICENSE](LICENSE) file for details.
408
436
  - [x] Extension for VS Code to list and highlight them like git (Major release)
409
437
  - [ ] MCP server (Minor release)
410
438
  - [ ] Encryption of files (Minor release)
411
- - [ ] Add stages for variables (Major release)
439
+ - [x] Add stages for variables (Major release)
412
440
  - [ ] Migrate dorky project to another storage (partially implemented)
413
441
  - [ ] Add more test cases
414
442
  - [ ] Deletion of files
415
443
  - [ ] Edge cases for failure when credentials are invalid
416
- - [ ] Add coverage reports badges
444
+ - [x] Add coverage reports badges
417
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dorky",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "DevOps Records Keeper.",
5
5
  "bin": {
6
6
  "dorky": "bin/index.js"