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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aethel",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Git-style Google Drive sync CLI with interactive TUI",
5
5
  "type": "module",
6
6
  "license": "MIT",
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(opts);
67
- const n = state.diff.changes.length;
68
- spinner.succeed(n ? `Loaded state — ${n} change(s) detected` : "Loaded state — everything up to date");
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
- spinner.succeed(`Snapshot saved: "${message}"`);
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) {
@@ -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
 
@@ -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
- Promise.resolve(readLatestSnapshot(this._root)),
120
- this._loadRemoteState({ useCache }),
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: computeDiff(snapshot, remote, local, { root: this._root }),
152
+ diff,
153
+ timings,
131
154
  };
132
155
  }
133
156