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 +7 -0
- package/README.md +12 -0
- package/package.json +2 -1
- package/src/core/diff.js +19 -0
- package/src/core/sync.js +25 -5
- package/src/tui/command-catalog.js +9 -0
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.
|
|
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
|
-
|
|
161
|
-
|
|
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,
|
|
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
|
];
|