aethel 0.2.6 → 0.3.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/CHANGELOG.md +4 -0
- package/README.md +27 -1
- package/package.json +1 -1
- package/src/cli.js +92 -181
- package/src/core/diff.js +7 -9
- package/src/core/drive-api.js +56 -13
- package/src/core/repository.js +309 -0
- package/src/core/snapshot.js +14 -3
- package/src/tui/app.js +530 -70
- package/src/tui/command-catalog.js +140 -0
- package/src/tui/commands.js +74 -0
- package/src/tui/index.js +12 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ Dual-pane file browser — local filesystem on the left, Google Drive on the rig
|
|
|
123
123
|
| `Space` | Toggle selection in Drive pane |
|
|
124
124
|
| `t` / `d` | Trash / permanently delete selected Drive items |
|
|
125
125
|
| `/` | Filter by name |
|
|
126
|
+
| `f` | Open the commands page and choose a TUI action |
|
|
127
|
+
| `:` | Run any Aethel CLI command inside the TUI |
|
|
126
128
|
|
|
127
129
|
## Ignore Patterns
|
|
128
130
|
|
|
@@ -147,7 +149,31 @@ build/
|
|
|
147
149
|
|
|
148
150
|
## Architecture
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
Aethel uses a **Repository pattern** — a single `Repository` class (`src/core/repository.js`) wraps all core modules and serves as the unified data-access layer for both the CLI and the TUI.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
src/
|
|
156
|
+
├── cli.js CLI entry — all handlers use Repository
|
|
157
|
+
├── core/
|
|
158
|
+
│ ├── repository.js Unified data-access layer
|
|
159
|
+
│ ├── auth.js OAuth authentication
|
|
160
|
+
│ ├── config.js Workspace config & state persistence
|
|
161
|
+
│ ├── diff.js Change detection between states
|
|
162
|
+
│ ├── drive-api.js Google Drive API wrapper
|
|
163
|
+
│ ├── local-fs.js Local filesystem operations
|
|
164
|
+
│ ├── remote-cache.js Short-lived remote file cache
|
|
165
|
+
│ ├── snapshot.js Local scanning & snapshot creation
|
|
166
|
+
│ ├── staging.js Stage/unstage operations
|
|
167
|
+
│ ├── sync.js Execute staged changes
|
|
168
|
+
│ └── ignore.js .aethelignore pattern matching
|
|
169
|
+
└── tui/
|
|
170
|
+
├── app.js React (Ink) dual-pane component
|
|
171
|
+
├── index.js TUI entry
|
|
172
|
+
├── commands.js CLI command parser for TUI
|
|
173
|
+
└── command-catalog.js Available TUI commands
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed module structure and data flow.
|
|
151
177
|
|
|
152
178
|
## Contributing
|
|
153
179
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,34 +3,18 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveCredentialsPath, resolveTokenPath } from "./core/auth.js";
|
|
7
7
|
import {
|
|
8
|
-
AETHEL_DIR,
|
|
9
|
-
HISTORY_DIR,
|
|
10
|
-
LATEST_SNAPSHOT,
|
|
11
|
-
SNAPSHOTS_DIR,
|
|
12
8
|
initWorkspace,
|
|
13
|
-
readConfig,
|
|
14
|
-
readLatestSnapshot,
|
|
15
9
|
requireRoot,
|
|
16
|
-
writeSnapshot,
|
|
17
10
|
} from "./core/config.js";
|
|
18
|
-
import { ChangeType
|
|
11
|
+
import { ChangeType } from "./core/diff.js";
|
|
19
12
|
import {
|
|
20
|
-
assertNoDuplicateFolders,
|
|
21
|
-
batchOperateFiles,
|
|
22
13
|
dedupeDuplicateFolders,
|
|
23
|
-
getRemoteState,
|
|
24
|
-
getAccountInfo,
|
|
25
|
-
listAccessibleFiles,
|
|
26
14
|
DuplicateFoldersError,
|
|
27
|
-
withDriveRetry,
|
|
28
15
|
} from "./core/drive-api.js";
|
|
29
16
|
import { createDefaultIgnoreFile, loadIgnoreRules } from "./core/ignore.js";
|
|
30
|
-
import {
|
|
31
|
-
import { buildSnapshot, scanLocal } from "./core/snapshot.js";
|
|
32
|
-
import { stageChange, stageChanges, stageConflictResolution, stagedEntries, unstageAll, unstagePath } from "./core/staging.js";
|
|
33
|
-
import { executeStaged } from "./core/sync.js";
|
|
17
|
+
import { Repository } from "./core/repository.js";
|
|
34
18
|
import { runTui } from "./tui/index.js";
|
|
35
19
|
|
|
36
20
|
const REQUIRED_CONFIRMATION = "DELETE ALL MY GOOGLE DRIVE FILES";
|
|
@@ -41,22 +25,14 @@ function addAuthOptions(command) {
|
|
|
41
25
|
.option("--token <path>", "Path to cached OAuth token JSON");
|
|
42
26
|
}
|
|
43
27
|
|
|
44
|
-
async function
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!remoteState) {
|
|
54
|
-
remoteState = await getRemoteState(drive, rootFolderId);
|
|
55
|
-
writeRemoteCache(root, remoteState, rootFolderId);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
assertNoDuplicateFolders(remoteState.duplicateFolders);
|
|
59
|
-
return remoteState;
|
|
28
|
+
async function openRepo(options, { requireWorkspace = true } = {}) {
|
|
29
|
+
const root = requireWorkspace ? requireRoot() : null;
|
|
30
|
+
const repo = new Repository(root, {
|
|
31
|
+
credentials: options.credentials,
|
|
32
|
+
token: options.token,
|
|
33
|
+
});
|
|
34
|
+
await repo.connect();
|
|
35
|
+
return repo;
|
|
60
36
|
}
|
|
61
37
|
|
|
62
38
|
function matchesPattern(targetPath, pattern) {
|
|
@@ -122,33 +98,9 @@ function requireConfirmation(options) {
|
|
|
122
98
|
}
|
|
123
99
|
}
|
|
124
100
|
|
|
125
|
-
async function loadWorkspaceState(root, options, { useCache = true } = {}) {
|
|
126
|
-
const config = readConfig(root);
|
|
127
|
-
|
|
128
|
-
// Start auth, local scan, and snapshot read all in parallel.
|
|
129
|
-
const [drive, local, snapshot] = await Promise.all([
|
|
130
|
-
getDrive(options),
|
|
131
|
-
scanLocal(root),
|
|
132
|
-
Promise.resolve(readLatestSnapshot(root)),
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
// Try the short-lived remote cache first (saves a full API round-trip)
|
|
136
|
-
const remoteState = await loadRemoteState(root, drive, config, { useCache });
|
|
137
|
-
const remote = remoteState.files;
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
config,
|
|
141
|
-
drive,
|
|
142
|
-
remote,
|
|
143
|
-
local,
|
|
144
|
-
snapshot,
|
|
145
|
-
diff: computeDiff(snapshot, remote, local, { root }),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
101
|
async function handleAuth(options) {
|
|
150
|
-
const
|
|
151
|
-
const account = await getAccountInfo(
|
|
102
|
+
const repo = await openRepo(options, { requireWorkspace: false });
|
|
103
|
+
const account = await repo.getAccountInfo();
|
|
152
104
|
|
|
153
105
|
console.log("OAuth initialization completed.");
|
|
154
106
|
console.log(`Credentials path: ${resolveCredentialsPath(options.credentials)}`);
|
|
@@ -161,8 +113,8 @@ async function handleAuth(options) {
|
|
|
161
113
|
|
|
162
114
|
async function handleClean(options) {
|
|
163
115
|
requireConfirmation(options);
|
|
164
|
-
const
|
|
165
|
-
const files = await
|
|
116
|
+
const repo = await openRepo(options, { requireWorkspace: false });
|
|
117
|
+
const files = await repo.listRemoteFiles({ includeSharedDrives: Boolean(options.sharedDrives) });
|
|
166
118
|
|
|
167
119
|
printCleanerPlan(files, options);
|
|
168
120
|
|
|
@@ -176,7 +128,7 @@ async function handleClean(options) {
|
|
|
176
128
|
return;
|
|
177
129
|
}
|
|
178
130
|
|
|
179
|
-
const result = await batchOperateFiles(
|
|
131
|
+
const result = await repo.batchOperateFiles(files, {
|
|
180
132
|
permanent: Boolean(options.permanent),
|
|
181
133
|
includeSharedDrives: Boolean(options.sharedDrives),
|
|
182
134
|
onProgress: (done, total, verb, name) => {
|
|
@@ -219,9 +171,9 @@ async function handleInit(options) {
|
|
|
219
171
|
}
|
|
220
172
|
|
|
221
173
|
async function handleStatus(options) {
|
|
222
|
-
const
|
|
223
|
-
const { diff } = await
|
|
224
|
-
const staged =
|
|
174
|
+
const repo = await openRepo(options);
|
|
175
|
+
const { diff } = await repo.loadState();
|
|
176
|
+
const staged = repo.getStagedEntries();
|
|
225
177
|
|
|
226
178
|
if (diff.isClean && staged.length === 0) {
|
|
227
179
|
console.log("Everything up to date.");
|
|
@@ -258,8 +210,8 @@ async function handleStatus(options) {
|
|
|
258
210
|
}
|
|
259
211
|
|
|
260
212
|
async function handleDiff(options) {
|
|
261
|
-
const
|
|
262
|
-
const { diff } = await
|
|
213
|
+
const repo = await openRepo(options);
|
|
214
|
+
const { diff } = await repo.loadState();
|
|
263
215
|
|
|
264
216
|
if (diff.isClean) {
|
|
265
217
|
console.log("No changes detected.");
|
|
@@ -292,14 +244,14 @@ async function handleDiff(options) {
|
|
|
292
244
|
}
|
|
293
245
|
|
|
294
246
|
async function handleAdd(paths, options) {
|
|
295
|
-
const
|
|
296
|
-
const { diff } = await
|
|
247
|
+
const repo = await openRepo(options);
|
|
248
|
+
const { diff } = await repo.loadState();
|
|
297
249
|
|
|
298
250
|
if (options.all) {
|
|
299
251
|
const toStage = diff.changes.filter(
|
|
300
252
|
(change) => change.suggestedAction !== "conflict"
|
|
301
253
|
);
|
|
302
|
-
const count = stageChanges(
|
|
254
|
+
const count = repo.stageChanges(toStage);
|
|
303
255
|
console.log(`Staged ${count} change(s).`);
|
|
304
256
|
return;
|
|
305
257
|
}
|
|
@@ -323,7 +275,7 @@ async function handleAdd(paths, options) {
|
|
|
323
275
|
continue;
|
|
324
276
|
}
|
|
325
277
|
|
|
326
|
-
stageChange(
|
|
278
|
+
repo.stageChange(change);
|
|
327
279
|
stagedCount += 1;
|
|
328
280
|
console.log(` Staged: ${change.path}`);
|
|
329
281
|
}
|
|
@@ -334,15 +286,16 @@ async function handleAdd(paths, options) {
|
|
|
334
286
|
|
|
335
287
|
function handleReset(paths, options) {
|
|
336
288
|
const root = requireRoot();
|
|
289
|
+
const repo = new Repository(root);
|
|
337
290
|
|
|
338
291
|
if (options.all) {
|
|
339
|
-
const count = unstageAll(
|
|
292
|
+
const count = repo.unstageAll();
|
|
340
293
|
console.log(`Unstaged ${count} change(s).`);
|
|
341
294
|
return;
|
|
342
295
|
}
|
|
343
296
|
|
|
344
297
|
for (const targetPath of paths || []) {
|
|
345
|
-
if (unstagePath(
|
|
298
|
+
if (repo.unstagePath(targetPath)) {
|
|
346
299
|
console.log(` Unstaged: ${targetPath}`);
|
|
347
300
|
continue;
|
|
348
301
|
}
|
|
@@ -351,23 +304,20 @@ function handleReset(paths, options) {
|
|
|
351
304
|
}
|
|
352
305
|
}
|
|
353
306
|
|
|
354
|
-
async function handleCommit(options) {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const staged = stagedEntries(root);
|
|
307
|
+
async function handleCommit(options, { repo: existingRepo } = {}) {
|
|
308
|
+
const repo = existingRepo || await openRepo(options);
|
|
309
|
+
const staged = repo.getStagedEntries();
|
|
358
310
|
|
|
359
311
|
if (!staged.length) {
|
|
360
312
|
console.log("Nothing staged. Use 'aethel add' first.");
|
|
361
313
|
return;
|
|
362
314
|
}
|
|
363
315
|
|
|
364
|
-
const drive = await getDrive(options);
|
|
365
316
|
const message = options.message || "sync";
|
|
366
|
-
await loadRemoteState(root, drive, config, { useCache: true });
|
|
367
317
|
|
|
368
318
|
console.log(`Committing ${staged.length} change(s)...`);
|
|
369
319
|
|
|
370
|
-
const result = await executeStaged(
|
|
320
|
+
const result = await repo.executeStaged((done, total, verb, name) => {
|
|
371
321
|
if (done < total) {
|
|
372
322
|
console.log(` [${done + 1}/${total}] ${verb}: ${name}`);
|
|
373
323
|
}
|
|
@@ -381,47 +331,21 @@ async function handleCommit(options) {
|
|
|
381
331
|
}
|
|
382
332
|
|
|
383
333
|
console.log("Saving snapshot...");
|
|
384
|
-
|
|
385
|
-
const [remoteState, local] = await Promise.all([
|
|
386
|
-
getRemoteState(drive, config.drive_folder_id || null),
|
|
387
|
-
scanLocal(root),
|
|
388
|
-
]);
|
|
389
|
-
assertNoDuplicateFolders(remoteState.duplicateFolders);
|
|
390
|
-
writeRemoteCache(root, remoteState, config.drive_folder_id || null);
|
|
391
|
-
writeSnapshot(root, buildSnapshot(remoteState.files, local, message));
|
|
334
|
+
await repo.saveSnapshot(message);
|
|
392
335
|
console.log(`Snapshot saved: "${message}"`);
|
|
393
336
|
}
|
|
394
337
|
|
|
395
338
|
function handleLog(options) {
|
|
396
339
|
const root = requireRoot();
|
|
397
|
-
const
|
|
398
|
-
const entries =
|
|
399
|
-
const latestPath = path.join(snapshotsPath, LATEST_SNAPSHOT);
|
|
400
|
-
|
|
401
|
-
if (fs.existsSync(latestPath)) {
|
|
402
|
-
entries.push(JSON.parse(fs.readFileSync(latestPath, "utf8")));
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const historyPath = path.join(snapshotsPath, HISTORY_DIR);
|
|
406
|
-
if (fs.existsSync(historyPath)) {
|
|
407
|
-
const historyFiles = fs
|
|
408
|
-
.readdirSync(historyPath)
|
|
409
|
-
.filter((fileName) => fileName.endsWith(".json"))
|
|
410
|
-
.sort()
|
|
411
|
-
.reverse();
|
|
412
|
-
|
|
413
|
-
for (const fileName of historyFiles) {
|
|
414
|
-
const fullPath = path.join(historyPath, fileName);
|
|
415
|
-
entries.push(JSON.parse(fs.readFileSync(fullPath, "utf8")));
|
|
416
|
-
}
|
|
417
|
-
}
|
|
340
|
+
const repo = new Repository(root);
|
|
341
|
+
const entries = repo.getHistory(options.limit || 10);
|
|
418
342
|
|
|
419
343
|
if (!entries.length) {
|
|
420
344
|
console.log("No commits yet.");
|
|
421
345
|
return;
|
|
422
346
|
}
|
|
423
347
|
|
|
424
|
-
for (const snapshot of entries
|
|
348
|
+
for (const snapshot of entries) {
|
|
425
349
|
console.log(
|
|
426
350
|
` ${snapshot.timestamp || "?"} ${snapshot.message || "(no message)"} (${Object.keys(snapshot.files || {}).length} files)`
|
|
427
351
|
);
|
|
@@ -429,23 +353,18 @@ function handleLog(options) {
|
|
|
429
353
|
}
|
|
430
354
|
|
|
431
355
|
async function handleFetch(options) {
|
|
432
|
-
const
|
|
433
|
-
const config = readConfig(root);
|
|
434
|
-
const drive = await getDrive(options);
|
|
435
|
-
const snapshot = readLatestSnapshot(root);
|
|
356
|
+
const repo = await openRepo(options);
|
|
436
357
|
|
|
437
|
-
invalidateRemoteCache(
|
|
358
|
+
repo.invalidateRemoteCache();
|
|
438
359
|
console.log("Fetching remote file list...");
|
|
439
|
-
const remoteState = await getRemoteState(
|
|
440
|
-
writeRemoteCache(root, remoteState, config.drive_folder_id || null);
|
|
441
|
-
assertNoDuplicateFolders(remoteState.duplicateFolders);
|
|
360
|
+
const remoteState = await repo.getRemoteState({ useCache: false });
|
|
442
361
|
const remote = remoteState.files;
|
|
443
362
|
console.log(`Found ${remote.length} file(s) on Drive.`);
|
|
444
363
|
|
|
445
|
-
|
|
364
|
+
const snapshot = repo.getSnapshot();
|
|
446
365
|
if (snapshot) {
|
|
447
|
-
const local = await scanLocal(
|
|
448
|
-
const diff = computeDiff(snapshot, remote, local
|
|
366
|
+
const local = await repo.scanLocal();
|
|
367
|
+
const diff = repo.computeDiff(snapshot, remote, local);
|
|
449
368
|
const remoteChanges = diff.remoteChanges;
|
|
450
369
|
const conflicts = diff.conflicts;
|
|
451
370
|
|
|
@@ -472,8 +391,8 @@ async function handleFetch(options) {
|
|
|
472
391
|
}
|
|
473
392
|
|
|
474
393
|
async function handlePull(paths, options) {
|
|
475
|
-
const
|
|
476
|
-
const { diff } = await
|
|
394
|
+
const repo = await openRepo(options);
|
|
395
|
+
const { diff } = await repo.loadState({ useCache: false });
|
|
477
396
|
|
|
478
397
|
let remoteChanges = diff.changes.filter((change) =>
|
|
479
398
|
[
|
|
@@ -483,18 +402,16 @@ async function handlePull(paths, options) {
|
|
|
483
402
|
].includes(change.changeType)
|
|
484
403
|
);
|
|
485
404
|
|
|
486
|
-
// Include conflicts resolved as "theirs" when --force is set
|
|
487
405
|
if (options.force) {
|
|
488
406
|
const conflicts = diff.conflicts;
|
|
489
407
|
if (conflicts.length) {
|
|
490
408
|
console.log(`Force-pulling ${conflicts.length} conflict(s) (remote wins)...`);
|
|
491
409
|
for (const c of conflicts) {
|
|
492
|
-
stageConflictResolution(
|
|
410
|
+
repo.stageConflictResolution(c, "theirs");
|
|
493
411
|
}
|
|
494
412
|
}
|
|
495
413
|
}
|
|
496
414
|
|
|
497
|
-
// Filter to specific paths if provided
|
|
498
415
|
if (paths && paths.length > 0) {
|
|
499
416
|
remoteChanges = remoteChanges.filter((change) =>
|
|
500
417
|
paths.some((p) => matchesPattern(change.path, p))
|
|
@@ -517,14 +434,14 @@ async function handlePull(paths, options) {
|
|
|
517
434
|
return;
|
|
518
435
|
}
|
|
519
436
|
|
|
520
|
-
const count = stageChanges(
|
|
437
|
+
const count = repo.stageChanges(remoteChanges);
|
|
521
438
|
console.log(`Staged ${count} remote change(s). Committing...`);
|
|
522
|
-
await handleCommit({ ...options, message: options.message || "pull" });
|
|
439
|
+
await handleCommit({ ...options, message: options.message || "pull" }, { repo });
|
|
523
440
|
}
|
|
524
441
|
|
|
525
442
|
async function handlePush(paths, options) {
|
|
526
|
-
const
|
|
527
|
-
const { diff } = await
|
|
443
|
+
const repo = await openRepo(options);
|
|
444
|
+
const { diff } = await repo.loadState({ useCache: false });
|
|
528
445
|
|
|
529
446
|
let localChanges = diff.changes.filter((change) =>
|
|
530
447
|
[
|
|
@@ -534,18 +451,16 @@ async function handlePush(paths, options) {
|
|
|
534
451
|
].includes(change.changeType)
|
|
535
452
|
);
|
|
536
453
|
|
|
537
|
-
// Include conflicts resolved as "ours" when --force is set
|
|
538
454
|
if (options.force) {
|
|
539
455
|
const conflicts = diff.conflicts;
|
|
540
456
|
if (conflicts.length) {
|
|
541
457
|
console.log(`Force-pushing ${conflicts.length} conflict(s) (local wins)...`);
|
|
542
458
|
for (const c of conflicts) {
|
|
543
|
-
stageConflictResolution(
|
|
459
|
+
repo.stageConflictResolution(c, "ours");
|
|
544
460
|
}
|
|
545
461
|
}
|
|
546
462
|
}
|
|
547
463
|
|
|
548
|
-
// Filter to specific paths if provided
|
|
549
464
|
if (paths && paths.length > 0) {
|
|
550
465
|
localChanges = localChanges.filter((change) =>
|
|
551
466
|
paths.some((p) => matchesPattern(change.path, p))
|
|
@@ -568,14 +483,14 @@ async function handlePush(paths, options) {
|
|
|
568
483
|
return;
|
|
569
484
|
}
|
|
570
485
|
|
|
571
|
-
const count = stageChanges(
|
|
486
|
+
const count = repo.stageChanges(localChanges);
|
|
572
487
|
console.log(`Staged ${count} local change(s). Committing...`);
|
|
573
|
-
await handleCommit({ ...options, message: options.message || "push" });
|
|
488
|
+
await handleCommit({ ...options, message: options.message || "push" }, { repo });
|
|
574
489
|
}
|
|
575
490
|
|
|
576
491
|
async function handleResolve(paths, options) {
|
|
577
|
-
const
|
|
578
|
-
const { diff } = await
|
|
492
|
+
const repo = await openRepo(options);
|
|
493
|
+
const { diff } = await repo.loadState();
|
|
579
494
|
const conflicts = diff.conflicts;
|
|
580
495
|
|
|
581
496
|
if (conflicts.length === 0) {
|
|
@@ -618,7 +533,7 @@ async function handleResolve(paths, options) {
|
|
|
618
533
|
const strategyLabel = { ours: "local wins", theirs: "remote wins", both: "keep both" };
|
|
619
534
|
|
|
620
535
|
for (const conflict of toResolve) {
|
|
621
|
-
stageConflictResolution(
|
|
536
|
+
repo.stageConflictResolution(conflict, strategy);
|
|
622
537
|
console.log(` Resolved: ${conflict.path} → ${strategyLabel[strategy]}`);
|
|
623
538
|
}
|
|
624
539
|
|
|
@@ -669,34 +584,24 @@ function handleIgnore(subcommand, args) {
|
|
|
669
584
|
|
|
670
585
|
function handleShow(ref, options) {
|
|
671
586
|
const root = requireRoot();
|
|
672
|
-
const
|
|
587
|
+
const repo = new Repository(root);
|
|
673
588
|
|
|
674
|
-
|
|
589
|
+
const snapshot = repo.getSnapshotByRef(ref);
|
|
675
590
|
|
|
676
|
-
if (!
|
|
677
|
-
|
|
678
|
-
if (!snapshot) {
|
|
591
|
+
if (!snapshot) {
|
|
592
|
+
if (!ref || ref === "HEAD" || ref === "latest") {
|
|
679
593
|
console.log("No commits yet.");
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
} else {
|
|
683
|
-
// Try to match a history file by prefix
|
|
684
|
-
const historyPath = path.join(snapshotsPath, HISTORY_DIR);
|
|
685
|
-
if (!fs.existsSync(historyPath)) {
|
|
686
|
-
console.log("No commit history found.");
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
const files = fs.readdirSync(historyPath).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
690
|
-
const match = files.find((f) => f.startsWith(ref));
|
|
691
|
-
if (!match) {
|
|
594
|
+
} else {
|
|
692
595
|
console.log(`No snapshot matching '${ref}' found.`);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
console.log(
|
|
596
|
+
const history = repo.getHistory(10);
|
|
597
|
+
if (history.length) {
|
|
598
|
+
console.log("Available snapshots:");
|
|
599
|
+
for (const s of history) {
|
|
600
|
+
console.log(` ${s.timestamp || "?"}`);
|
|
601
|
+
}
|
|
696
602
|
}
|
|
697
|
-
return;
|
|
698
603
|
}
|
|
699
|
-
|
|
604
|
+
return;
|
|
700
605
|
}
|
|
701
606
|
|
|
702
607
|
console.log(`Snapshot: ${snapshot.timestamp || "?"}`);
|
|
@@ -738,16 +643,15 @@ function handleShow(ref, options) {
|
|
|
738
643
|
}
|
|
739
644
|
|
|
740
645
|
async function handleRestore(paths, options) {
|
|
741
|
-
const
|
|
742
|
-
const
|
|
743
|
-
const snapshot = readLatestSnapshot(root);
|
|
646
|
+
const repo = await openRepo(options);
|
|
647
|
+
const snapshot = repo.getSnapshot();
|
|
744
648
|
|
|
745
649
|
if (!snapshot) {
|
|
746
650
|
console.log("No snapshot to restore from. Run 'aethel commit' first.");
|
|
747
651
|
return;
|
|
748
652
|
}
|
|
749
653
|
|
|
750
|
-
const
|
|
654
|
+
const root = repo.root;
|
|
751
655
|
const remoteFiles = snapshot.files || {};
|
|
752
656
|
|
|
753
657
|
for (const targetPath of paths) {
|
|
@@ -765,13 +669,13 @@ async function handleRestore(paths, options) {
|
|
|
765
669
|
console.log(` Restoring ${targetPath} from Drive...`);
|
|
766
670
|
|
|
767
671
|
try {
|
|
768
|
-
const meta = await drive.files.get({
|
|
672
|
+
const meta = await repo.drive.files.get({
|
|
769
673
|
fileId: entry.id,
|
|
770
674
|
fields: "id,name,mimeType",
|
|
771
675
|
});
|
|
772
676
|
|
|
773
677
|
const { downloadFile } = await import("./core/drive-api.js");
|
|
774
|
-
await downloadFile(drive, { ...meta.data, id: entry.id }, localDest);
|
|
678
|
+
await downloadFile(repo.drive, { ...meta.data, id: entry.id }, localDest);
|
|
775
679
|
console.log(` Restored: ${targetPath}`);
|
|
776
680
|
} catch (err) {
|
|
777
681
|
console.log(` Failed to restore ${targetPath}: ${err.message}`);
|
|
@@ -780,23 +684,22 @@ async function handleRestore(paths, options) {
|
|
|
780
684
|
}
|
|
781
685
|
|
|
782
686
|
async function handleRm(paths, options) {
|
|
783
|
-
const
|
|
784
|
-
const { diff } = await
|
|
687
|
+
const repo = await openRepo(options);
|
|
688
|
+
const { diff } = await repo.loadState();
|
|
689
|
+
const root = repo.root;
|
|
785
690
|
|
|
786
691
|
for (const targetPath of paths) {
|
|
787
|
-
// Delete locally
|
|
788
692
|
const localAbs = path.join(root, targetPath);
|
|
789
693
|
if (fs.existsSync(localAbs)) {
|
|
790
694
|
await fs.promises.rm(localAbs, { recursive: true });
|
|
791
695
|
console.log(` Deleted locally: ${targetPath}`);
|
|
792
696
|
}
|
|
793
697
|
|
|
794
|
-
// If it exists on remote, stage a delete_remote
|
|
795
698
|
const remoteChange = diff.changes.find(
|
|
796
699
|
(c) => c.path === targetPath && c.fileId
|
|
797
700
|
);
|
|
798
701
|
if (remoteChange) {
|
|
799
|
-
stageChange(
|
|
702
|
+
repo.stageChange({
|
|
800
703
|
...remoteChange,
|
|
801
704
|
changeType: ChangeType.LOCAL_DELETED,
|
|
802
705
|
suggestedAction: "delete_remote",
|
|
@@ -828,10 +731,19 @@ async function handleMv(source, dest, options) {
|
|
|
828
731
|
}
|
|
829
732
|
|
|
830
733
|
async function handleTui(options) {
|
|
831
|
-
const
|
|
734
|
+
const repo = await openRepo(options, { requireWorkspace: false });
|
|
735
|
+
const cliArgs = [];
|
|
736
|
+
if (options.credentials) {
|
|
737
|
+
cliArgs.push("--credentials", options.credentials);
|
|
738
|
+
}
|
|
739
|
+
if (options.token) {
|
|
740
|
+
cliArgs.push("--token", options.token);
|
|
741
|
+
}
|
|
832
742
|
await runTui({
|
|
833
|
-
|
|
743
|
+
repo,
|
|
834
744
|
includeSharedDrives: Boolean(options.sharedDrives),
|
|
745
|
+
cliPath: path.resolve(process.argv[1]),
|
|
746
|
+
cliArgs,
|
|
835
747
|
});
|
|
836
748
|
}
|
|
837
749
|
|
|
@@ -850,12 +762,11 @@ function printDedupeSummary(result) {
|
|
|
850
762
|
}
|
|
851
763
|
|
|
852
764
|
async function handleDedupeFolders(options) {
|
|
853
|
-
const
|
|
854
|
-
const config =
|
|
855
|
-
const drive = await getDrive(options);
|
|
765
|
+
const repo = await openRepo(options);
|
|
766
|
+
const config = repo.getConfig();
|
|
856
767
|
const rootFolderId = config.drive_folder_id || null;
|
|
857
|
-
const ignoreRules = loadIgnoreRules(root);
|
|
858
|
-
const result = await dedupeDuplicateFolders(drive, rootFolderId, {
|
|
768
|
+
const ignoreRules = loadIgnoreRules(repo.root);
|
|
769
|
+
const result = await dedupeDuplicateFolders(repo.drive, rootFolderId, {
|
|
859
770
|
execute: Boolean(options.execute),
|
|
860
771
|
ignoreRules,
|
|
861
772
|
onProgress: (event) => {
|
|
@@ -893,7 +804,7 @@ async function handleDedupeFolders(options) {
|
|
|
893
804
|
printDedupeSummary(result);
|
|
894
805
|
|
|
895
806
|
if (options.execute) {
|
|
896
|
-
invalidateRemoteCache(
|
|
807
|
+
repo.invalidateRemoteCache();
|
|
897
808
|
if (result.remainingDuplicateFolders.length > 0) {
|
|
898
809
|
throw new DuplicateFoldersError(result.remainingDuplicateFolders);
|
|
899
810
|
}
|
package/src/core/diff.js
CHANGED
|
@@ -163,16 +163,12 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
163
163
|
const changes = [];
|
|
164
164
|
const snapshotFiles = snapshot?.files || {};
|
|
165
165
|
const snapshotLocalFiles = snapshot?.localFiles || {};
|
|
166
|
-
const snapshotById = new Map();
|
|
167
|
-
|
|
168
|
-
for (const [fileId, meta] of Object.entries(snapshotFiles)) {
|
|
169
|
-
snapshotById.set(fileId, meta);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const remoteById = new Map(remoteFiles.map((file) => [file.id, file]));
|
|
173
166
|
|
|
167
|
+
// Build remote lookup and detect additions/modifications in one pass
|
|
168
|
+
const remoteById = new Map();
|
|
174
169
|
for (const remoteFile of remoteFiles) {
|
|
175
|
-
|
|
170
|
+
remoteById.set(remoteFile.id, remoteFile);
|
|
171
|
+
const snapshotEntry = snapshotFiles[remoteFile.id];
|
|
176
172
|
|
|
177
173
|
if (!snapshotEntry) {
|
|
178
174
|
changes.push(
|
|
@@ -199,8 +195,10 @@ export function computeDiff(snapshot, remoteFiles, localFiles, { root, respectIg
|
|
|
199
195
|
}
|
|
200
196
|
}
|
|
201
197
|
|
|
202
|
-
|
|
198
|
+
// Detect remote deletions — snapshot entries missing from remote
|
|
199
|
+
for (const fileId of Object.keys(snapshotFiles)) {
|
|
203
200
|
if (!remoteById.has(fileId)) {
|
|
201
|
+
const snapshotEntry = snapshotFiles[fileId];
|
|
204
202
|
changes.push(
|
|
205
203
|
createChange({
|
|
206
204
|
changeType: ChangeType.REMOTE_DELETED,
|