aethel 0.3.6 → 0.3.7
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/package.json +1 -1
- package/src/cli.js +35 -5
- package/src/core/drive-api.js +15 -0
- package/src/core/repository.js +28 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.7 (2026-04-05)
|
|
4
|
+
|
|
5
|
+
- Fix orphan checker not recognizing My Drive root — all files under synced folders were silently dropped
|
|
6
|
+
|
|
3
7
|
## 0.3.6 (2026-04-05)
|
|
4
8
|
|
|
5
9
|
- Optimize status and saveSnapshot performance: parallelize loadState, skip redundant fetches, increase hash concurrency
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -60,12 +60,36 @@ async function openRepo(options, { requireWorkspace = true, silent = false } = {
|
|
|
60
60
|
return repo;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function fmtMs(ms) {
|
|
64
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
async function loadStateWithProgress(repo, opts) {
|
|
64
68
|
const spinner = createSpinner("Loading workspace state...");
|
|
65
69
|
try {
|
|
66
|
-
const state = await repo.loadState(
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const state = await repo.loadState({
|
|
71
|
+
...opts,
|
|
72
|
+
onPhase(phase, ms) {
|
|
73
|
+
if (phase === "local") spinner.update(`Scanned local files (${fmtMs(ms)}), waiting for remote...`);
|
|
74
|
+
else if (phase === "remote") spinner.update(`Fetched remote state (${fmtMs(ms)}), computing diff...`);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const { timings, diff } = state;
|
|
78
|
+
const n = diff.changes.length;
|
|
79
|
+
|
|
80
|
+
const parts = [
|
|
81
|
+
`${timings.localFiles} local`,
|
|
82
|
+
`${timings.remoteFiles} remote`,
|
|
83
|
+
];
|
|
84
|
+
const times = [
|
|
85
|
+
`scan ${fmtMs(timings.localMs)}`,
|
|
86
|
+
timings.remoteCached ? `remote cache hit` : `fetch ${fmtMs(timings.remoteMs)}`,
|
|
87
|
+
`diff ${fmtMs(timings.diffMs)}`,
|
|
88
|
+
`total ${fmtMs(timings.totalMs)}`,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const summary = n ? `${n} change(s)` : "up to date";
|
|
92
|
+
spinner.succeed(`${summary} (${parts.join(", ")}) [${times.join(" | ")}]`);
|
|
69
93
|
return state;
|
|
70
94
|
} catch (err) {
|
|
71
95
|
spinner.fail("Failed to load workspace state");
|
|
@@ -408,11 +432,16 @@ async function handleCommit(options, { repo: existingRepo, snapshotHint } = {})
|
|
|
408
432
|
}
|
|
409
433
|
}
|
|
410
434
|
|
|
435
|
+
const snapshotStart = Date.now();
|
|
411
436
|
const spinner = createSpinner("Saving snapshot...");
|
|
412
437
|
// snapshotHint lets callers (pull/push) pass pre-loaded state
|
|
413
438
|
// so saveSnapshot skips redundant API calls / fs scans.
|
|
414
439
|
await repo.saveSnapshot(message, snapshotHint);
|
|
415
|
-
|
|
440
|
+
const skipped = [];
|
|
441
|
+
if (snapshotHint?.remote) skipped.push("remote reused");
|
|
442
|
+
if (snapshotHint?.local) skipped.push("local reused");
|
|
443
|
+
const hint = skipped.length ? ` (${skipped.join(", ")})` : "";
|
|
444
|
+
spinner.succeed(`Snapshot saved in ${fmtMs(Date.now() - snapshotStart)}${hint}`);
|
|
416
445
|
}
|
|
417
446
|
|
|
418
447
|
function handleLog(options) {
|
|
@@ -436,10 +465,11 @@ async function handleFetch(options) {
|
|
|
436
465
|
const repo = await openRepo(options);
|
|
437
466
|
|
|
438
467
|
repo.invalidateRemoteCache();
|
|
468
|
+
const fetchStart = Date.now();
|
|
439
469
|
const spinner = createSpinner("Fetching remote file list...");
|
|
440
470
|
const remoteState = await repo.getRemoteState({ useCache: false });
|
|
441
471
|
const remote = remoteState.files;
|
|
442
|
-
spinner.succeed(`Found ${remote.length} file(s) on Drive`);
|
|
472
|
+
spinner.succeed(`Found ${remote.length} file(s) on Drive [${fmtMs(Date.now() - fetchStart)}]`);
|
|
443
473
|
|
|
444
474
|
const snapshot = repo.getSnapshot();
|
|
445
475
|
if (snapshot) {
|
package/src/core/drive-api.js
CHANGED
|
@@ -226,6 +226,15 @@ async function fetchAllItems(drive, { fields, includeSharedDrives = false } = {}
|
|
|
226
226
|
const files = [];
|
|
227
227
|
let pageToken = null;
|
|
228
228
|
|
|
229
|
+
// Fetch the real My Drive root folder metadata in parallel with
|
|
230
|
+
// the main list. Google Drive API returns actual folder IDs in
|
|
231
|
+
// `parents` (e.g. "0AJ…"), NOT the alias "root". Without this,
|
|
232
|
+
// the orphan checker can't recognise the root and marks every
|
|
233
|
+
// top-level folder as orphaned — silently dropping all files.
|
|
234
|
+
const rootPromise = drive.files
|
|
235
|
+
.get({ fileId: "root", fields: "id,name,mimeType,parents,createdTime" })
|
|
236
|
+
.catch(() => null);
|
|
237
|
+
|
|
229
238
|
const listOpts = {
|
|
230
239
|
q: "trashed = false",
|
|
231
240
|
fields: allFields,
|
|
@@ -252,6 +261,12 @@ async function fetchAllItems(drive, { fields, includeSharedDrives = false } = {}
|
|
|
252
261
|
pageToken = response.data.nextPageToken;
|
|
253
262
|
} while (pageToken);
|
|
254
263
|
|
|
264
|
+
// Add the real root folder so the orphan checker can walk up to it.
|
|
265
|
+
const rootRes = await rootPromise;
|
|
266
|
+
if (rootRes?.data?.id) {
|
|
267
|
+
folders.set(rootRes.data.id, rootRes.data);
|
|
268
|
+
}
|
|
269
|
+
|
|
255
270
|
return { folders, files };
|
|
256
271
|
}
|
|
257
272
|
|
package/src/core/repository.js
CHANGED
|
@@ -109,25 +109,48 @@ export class Repository {
|
|
|
109
109
|
* Load full workspace state in parallel, replacing the old
|
|
110
110
|
* loadWorkspaceState() helper from cli.js.
|
|
111
111
|
*/
|
|
112
|
-
async loadState({ useCache = true } = {}) {
|
|
112
|
+
async loadState({ useCache = true, onPhase } = {}) {
|
|
113
113
|
const config = this.getConfig();
|
|
114
|
+
const t0 = Date.now();
|
|
114
115
|
|
|
115
116
|
// Run all three in parallel — remote fetch is the slowest, overlap it
|
|
116
117
|
// with local scan and snapshot read.
|
|
118
|
+
const timings = {};
|
|
119
|
+
|
|
117
120
|
const [local, snapshot, remoteState] = await Promise.all([
|
|
118
|
-
scanLocal(this._root)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
scanLocal(this._root).then((r) => {
|
|
122
|
+
timings.localMs = Date.now() - t0;
|
|
123
|
+
onPhase?.("local", timings.localMs);
|
|
124
|
+
return r;
|
|
125
|
+
}),
|
|
126
|
+
Promise.resolve(readLatestSnapshot(this._root)).then((r) => {
|
|
127
|
+
timings.snapshotMs = Date.now() - t0;
|
|
128
|
+
return r;
|
|
129
|
+
}),
|
|
130
|
+
this._loadRemoteState({ useCache }).then((r) => {
|
|
131
|
+
timings.remoteMs = Date.now() - t0;
|
|
132
|
+
timings.remoteCached = useCache && timings.remoteMs < 100;
|
|
133
|
+
onPhase?.("remote", timings.remoteMs);
|
|
134
|
+
return r;
|
|
135
|
+
}),
|
|
121
136
|
]);
|
|
122
137
|
const remote = remoteState.files;
|
|
123
138
|
|
|
139
|
+
const diffStart = Date.now();
|
|
140
|
+
const diff = computeDiff(snapshot, remote, local, { root: this._root });
|
|
141
|
+
timings.diffMs = Date.now() - diffStart;
|
|
142
|
+
timings.totalMs = Date.now() - t0;
|
|
143
|
+
timings.localFiles = Object.keys(local).length;
|
|
144
|
+
timings.remoteFiles = remote.length;
|
|
145
|
+
|
|
124
146
|
return {
|
|
125
147
|
config,
|
|
126
148
|
remote,
|
|
127
149
|
remoteState,
|
|
128
150
|
local,
|
|
129
151
|
snapshot,
|
|
130
|
-
diff
|
|
152
|
+
diff,
|
|
153
|
+
timings,
|
|
131
154
|
};
|
|
132
155
|
}
|
|
133
156
|
|