aethel 1.2.0 → 1.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 (2026-04-26)
4
+
5
+ - Document the `verify` integrity-check command in the README help guide.
6
+ - Add `verify` to the TUI command catalog with local and remote verification actions.
7
+ - Fix legacy local-delete staging so remote deletions can resolve Drive file IDs from the latest snapshot.
8
+ - Add a debug installer command that symlinks the working-copy CLI as `debug_aethel`.
9
+
3
10
  ## 1.2.0 (2026-04-15)
4
11
 
5
12
  - Add Packing modules
package/README.md CHANGED
@@ -23,6 +23,7 @@ git clone https://github.com/CCJ-0617/Aethel.git
23
23
  cd Aethel
24
24
  npm install
25
25
  npm run install:cli # symlinks `aethel` into ~/.local/bin
26
+ npm run install:debug # symlinks `debug_aethel` without replacing `aethel`
26
27
  ```
27
28
 
28
29
  </details>
@@ -85,6 +86,7 @@ aethel commit -m "sync" # execute staged operations
85
86
  aethel pull -m "pull" # fetch remote changes and apply
86
87
  aethel pull --all # download the full remote tree to local
87
88
  aethel push -m "push" # push local changes to Drive
89
+ aethel verify # verify local files against the last snapshot
88
90
  ```
89
91
 
90
92
  `pull` applies remote changes relative to the latest snapshot. Use `pull --all` for the first full download or to rehydrate a local workspace from the current remote tree.
@@ -131,10 +133,20 @@ Processes deepest-first for single-pass convergence, caches child state to minim
131
133
  | `restore` | Restore files from the last snapshot |
132
134
  | `rm` | Remove local files and stage remote deletion |
133
135
  | `mv` | Move or rename local files |
136
+ | `verify` | Verify local and optional remote integrity against the last snapshot |
134
137
  | `clean` | List and optionally trash/delete Drive files |
135
138
  | `dedupe-folders` | Detect and merge duplicate remote folders |
136
139
  | `tui` | Launch interactive terminal UI |
137
140
 
141
+ ### Integrity Verification
142
+
143
+ ```bash
144
+ aethel verify # check snapshot checksum and local file hashes
145
+ aethel verify --remote # also compare Drive file hashes
146
+ ```
147
+
148
+ `verify` compares the latest snapshot with the workspace on disk and exits non-zero when files are missing or modified. Add `--remote` when you also want to verify Drive state before a release, migration, or restore.
149
+
138
150
  ## TUI
139
151
 
140
152
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aethel",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Git-style Google Drive sync CLI with interactive TUI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "auth": "node src/cli.js auth",
49
49
  "clean": "node src/cli.js clean",
50
50
  "install:cli": "bash scripts/install.sh",
51
+ "install:debug": "bash scripts/install-debug.sh",
51
52
  "release": "bash scripts/release.sh",
52
53
  "pack:check": "npm pack --dry-run",
53
54
  "prepublishOnly": "npm test && npm run pack:check",
package/src/core/diff.js CHANGED
@@ -306,6 +306,20 @@ function collectFolderPaths(filePaths) {
306
306
  return folders;
307
307
  }
308
308
 
309
+ function indexSnapshotFilesByPath(snapshotFiles) {
310
+ const byPath = new Map();
311
+
312
+ for (const [fileId, entry] of Object.entries(snapshotFiles || {})) {
313
+ for (const pathValue of [entry.path, entry.localPath]) {
314
+ if (pathValue && !byPath.has(pathValue)) {
315
+ byPath.set(pathValue, { fileId, entry });
316
+ }
317
+ }
318
+ }
319
+
320
+ return byPath;
321
+ }
322
+
309
323
  export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIgnore = true } = {}) {
310
324
  const ignoreRules = root && respectIgnore ? loadIgnoreRules(root) : null;
311
325
 
@@ -321,6 +335,7 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
321
335
  const changes = [];
322
336
  const snapshotFiles = snapshot?.files || {};
323
337
  const snapshotLocalFiles = snapshot?.localFiles || {};
338
+ const snapshotRemoteByPath = indexSnapshotFilesByPath(snapshotFiles);
324
339
 
325
340
  // Build sets of all folder paths that implicitly exist on each side
326
341
  // (from parent directories of files), so we can skip redundant folder additions.
@@ -409,10 +424,12 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
409
424
  }
410
425
 
411
426
  if (localChanged(snapshotEntry, localMeta)) {
427
+ const remoteEntry = snapshotRemoteByPath.get(relativePath);
412
428
  changes.push(
413
429
  createChange({
414
430
  changeType: ChangeType.LOCAL_MODIFIED,
415
431
  path: relativePath,
432
+ fileId: remoteEntry?.fileId || null,
416
433
  localMeta,
417
434
  snapshotMeta: snapshotEntry,
418
435
  })
@@ -426,10 +443,12 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
426
443
  if (snapshotEntry.isFolder && localFolderPaths.has(relativePath)) {
427
444
  continue;
428
445
  }
446
+ const remoteEntry = snapshotRemoteByPath.get(relativePath);
429
447
  changes.push(
430
448
  createChange({
431
449
  changeType: ChangeType.LOCAL_DELETED,
432
450
  path: relativePath,
451
+ fileId: remoteEntry?.fileId || null,
433
452
  snapshotMeta: snapshotEntry,
434
453
  })
435
454
  );
package/src/core/sync.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { readConfig, readIndex, writeIndex } from "./config.js";
3
+ import { readConfig, readIndex, readLatestSnapshot, writeIndex } from "./config.js";
4
4
  import { downloadFile, ensureFolder, trashFile, uploadFile } from "./drive-api.js";
5
5
  import { md5Local } from "./snapshot.js";
6
6
 
@@ -157,12 +157,31 @@ async function deleteLocalFile(entry, root) {
157
157
  }
158
158
  }
159
159
 
160
- async function deleteRemoteFile(drive, entry) {
161
- if (!entry.fileId) {
160
+ function findSnapshotFileIdByPath(snapshot, entry) {
161
+ const targetPaths = new Set(
162
+ [entry.remotePath, entry.path, entry.localPath].filter(Boolean)
163
+ );
164
+
165
+ for (const [fileId, snapshotEntry] of Object.entries(snapshot?.files || {})) {
166
+ if (
167
+ targetPaths.has(snapshotEntry.path) ||
168
+ targetPaths.has(snapshotEntry.localPath)
169
+ ) {
170
+ return fileId;
171
+ }
172
+ }
173
+
174
+ return null;
175
+ }
176
+
177
+ async function deleteRemoteFile(drive, entry, snapshot) {
178
+ const fileId = entry.fileId || findSnapshotFileIdByPath(snapshot, entry);
179
+
180
+ if (!fileId) {
162
181
  throw new Error("No fileId was found for delete_remote.");
163
182
  }
164
183
 
165
- await trashFile(drive, entry.fileId);
184
+ await trashFile(drive, fileId);
166
185
  }
167
186
 
168
187
  // ── Bounded-concurrency runner ───────────────────────────────────────
@@ -205,6 +224,7 @@ export async function executeStaged(drive, root, progress) {
205
224
  const config = readConfig(root);
206
225
  const index = readIndex(root);
207
226
  const staged = index.staged || [];
227
+ const snapshot = readLatestSnapshot(root);
208
228
  const driveFolderId = config.drive_folder_id || null;
209
229
  const result = new CommitResult();
210
230
 
@@ -248,7 +268,7 @@ export async function executeStaged(drive, root, progress) {
248
268
  if (entry.isFolder) result.foldersCreated++;
249
269
  else result.uploaded++;
250
270
  } else if (action === "delete_remote") {
251
- await deleteRemoteFile(drive, entry);
271
+ await deleteRemoteFile(drive, entry, snapshot);
252
272
  result.deletedRemote++;
253
273
  } else {
254
274
  throw new Error(`Unknown action '${action}'`);
@@ -137,4 +137,13 @@ export const COMMAND_CATALOG = [
137
137
  template: "mv old/path new/path",
138
138
  actions: [],
139
139
  },
140
+ {
141
+ name: "verify",
142
+ description: "Verify file integrity against the last snapshot",
143
+ template: "verify",
144
+ actions: [
145
+ { label: "Local Snapshot", command: "verify" },
146
+ { label: "Local and Remote", command: "verify --remote" },
147
+ ],
148
+ },
140
149
  ];