@seedvault/cli 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/sv.js +105 -46
  2. package/package.json +1 -1
package/dist/sv.js CHANGED
@@ -176,6 +176,10 @@ function createClient(serverUrl, token) {
176
176
  return res;
177
177
  }
178
178
  return {
179
+ async me() {
180
+ const res = await request("GET", "/v1/me");
181
+ return res.json();
182
+ },
179
183
  async signup(name, invite) {
180
184
  const body = { name };
181
185
  if (invite)
@@ -231,15 +235,12 @@ async function init(args) {
231
235
  }
232
236
  if (flags.server && flags.token) {
233
237
  const client = createClient(flags.server, flags.token);
238
+ let username;
234
239
  try {
235
- await client.health();
240
+ const me = await client.me();
241
+ username = me.username;
236
242
  } catch {
237
- console.error(`Could not reach server at ${flags.server}`);
238
- process.exit(1);
239
- }
240
- const username = flags["username"] || "";
241
- if (!username) {
242
- console.error("When using --token, also pass --username");
243
+ console.error(`Could not authenticate with server at ${flags.server}`);
243
244
  process.exit(1);
244
245
  }
245
246
  const config = {
@@ -303,12 +304,20 @@ async function init(args) {
303
304
  }
304
305
  const hasToken = await rl.question("Do you already have a token? (y/N): ");
305
306
  if (hasToken.toLowerCase() === "y") {
306
- const token = await rl.question("Token: ");
307
- const username = await rl.question("Username: ");
308
- const config = { server, token: token.trim(), username: username.trim(), collections: [] };
307
+ const token = (await rl.question("Token: ")).trim();
308
+ const authedClient = createClient(server, token);
309
+ let username;
310
+ try {
311
+ const me = await authedClient.me();
312
+ username = me.username;
313
+ } catch {
314
+ console.error(" Token is invalid or server rejected it.");
315
+ process.exit(1);
316
+ }
317
+ const config = { server, token, username, collections: [] };
309
318
  saveConfig(config);
310
319
  console.log(`
311
- Seedvault configured.`);
320
+ Seedvault configured as '${username}'`);
312
321
  } else {
313
322
  const name = await rl.question("Username (e.g. your-name-notes): ");
314
323
  const invite = await rl.question("Invite code (leave blank if first user): ");
@@ -333,6 +342,7 @@ Next steps:`);
333
342
  } finally {
334
343
  rl.close();
335
344
  }
345
+ process.exit(0);
336
346
  }
337
347
  function parseFlags(args) {
338
348
  const flags = {};
@@ -2126,6 +2136,8 @@ class RetryQueue {
2126
2136
  }
2127
2137
 
2128
2138
  // src/daemon/syncer.ts
2139
+ var SYNC_CONCURRENCY = 10;
2140
+
2129
2141
  class Syncer {
2130
2142
  client;
2131
2143
  username;
@@ -2167,6 +2179,7 @@ class Syncer {
2167
2179
  }
2168
2180
  const localFiles = await walkMd(collection.path);
2169
2181
  const localServerPaths = new Set;
2182
+ const toUpload = [];
2170
2183
  for (const localFile of localFiles) {
2171
2184
  const relPath = toPosixPath(relative5(collection.path, localFile.path));
2172
2185
  const serverPath = `${collection.name}/${relPath}`;
@@ -2181,22 +2194,24 @@ class Syncer {
2181
2194
  }
2182
2195
  }
2183
2196
  const content = await readFile(localFile.path, "utf-8");
2197
+ toUpload.push({ serverPath, content });
2198
+ }
2199
+ await pooled(toUpload, SYNC_CONCURRENCY, async (item) => {
2184
2200
  try {
2185
- await this.client.putFile(this.username, serverPath, content);
2201
+ await this.client.putFile(this.username, item.serverPath, item.content);
2186
2202
  uploaded++;
2187
2203
  } catch {
2188
2204
  this.queue.enqueue({
2189
2205
  type: "put",
2190
2206
  username: this.username,
2191
- serverPath,
2192
- content,
2207
+ serverPath: item.serverPath,
2208
+ content: item.content,
2193
2209
  queuedAt: new Date().toISOString()
2194
2210
  });
2195
2211
  }
2196
- }
2197
- for (const f of serverFiles) {
2198
- if (localServerPaths.has(f.path))
2199
- continue;
2212
+ });
2213
+ const toDelete = serverFiles.filter((f) => !localServerPaths.has(f.path));
2214
+ await pooled(toDelete, SYNC_CONCURRENCY, async (f) => {
2200
2215
  try {
2201
2216
  await this.client.deleteFile(this.username, f.path);
2202
2217
  deleted++;
@@ -2209,7 +2224,7 @@ class Syncer {
2209
2224
  queuedAt: new Date().toISOString()
2210
2225
  });
2211
2226
  }
2212
- }
2227
+ });
2213
2228
  this.log(` '${collection.name}': ${uploaded} uploaded, ${skipped} up-to-date, ${deleted} deleted`);
2214
2229
  } catch (e) {
2215
2230
  this.log(` '${collection.name}': sync failed (${e.message})`);
@@ -2222,7 +2237,7 @@ class Syncer {
2222
2237
  this.log(`Removing '${collection.name}' files from server...`);
2223
2238
  try {
2224
2239
  const { files: serverFiles } = await this.client.listFiles(this.username, collection.name + "/");
2225
- for (const f of serverFiles) {
2240
+ await pooled(serverFiles, SYNC_CONCURRENCY, async (f) => {
2226
2241
  try {
2227
2242
  await this.client.deleteFile(this.username, f.path);
2228
2243
  deleted++;
@@ -2236,7 +2251,7 @@ class Syncer {
2236
2251
  });
2237
2252
  queued++;
2238
2253
  }
2239
- }
2254
+ });
2240
2255
  this.log(` '${collection.name}': ${deleted} deleted, ${queued} queued`);
2241
2256
  } catch (e) {
2242
2257
  this.log(` '${collection.name}': remove failed (${e.message})`);
@@ -2280,6 +2295,16 @@ async function walkMd(dir) {
2280
2295
  await walkDirRecursive(dir, results);
2281
2296
  return results;
2282
2297
  }
2298
+ async function pooled(items, concurrency, fn) {
2299
+ let i = 0;
2300
+ async function worker() {
2301
+ while (i < items.length) {
2302
+ const idx = i++;
2303
+ await fn(items[idx]);
2304
+ }
2305
+ }
2306
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
2307
+ }
2283
2308
  async function walkDirRecursive(dir, results) {
2284
2309
  const entries = await readdir3(dir, { withFileTypes: true });
2285
2310
  for (const entry of entries) {
@@ -2646,7 +2671,7 @@ async function startForeground() {
2646
2671
  if (removedOverlappingCollections.length > 0) {
2647
2672
  config = normalizedConfig;
2648
2673
  }
2649
- const client = createClient(config.server, config.token);
2674
+ let client = createClient(config.server, config.token);
2650
2675
  try {
2651
2676
  await client.health();
2652
2677
  } catch {
@@ -2680,7 +2705,7 @@ async function startForeground() {
2680
2705
  log(` Collections: ${config.collections.map((f) => f.name).join(", ")}`);
2681
2706
  }
2682
2707
  writeFileSync2(getPidPath(), String(process.pid));
2683
- const syncer = new Syncer({
2708
+ let syncer = new Syncer({
2684
2709
  client,
2685
2710
  username: config.username,
2686
2711
  collections: config.collections,
@@ -2696,11 +2721,6 @@ async function startForeground() {
2696
2721
  log("Will continue watching for changes...");
2697
2722
  }
2698
2723
  }
2699
- const onWatcherEvent = (event) => {
2700
- syncer.handleEvent(event).catch((e) => {
2701
- log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
2702
- });
2703
- };
2704
2724
  let watcher = null;
2705
2725
  const rebuildWatcher = async (collections2) => {
2706
2726
  if (watcher) {
@@ -2711,13 +2731,17 @@ async function startForeground() {
2711
2731
  log("No collections configured. Daemon idle.");
2712
2732
  return;
2713
2733
  }
2714
- watcher = createWatcher(collections2, onWatcherEvent);
2734
+ watcher = createWatcher(collections2, (event) => {
2735
+ syncer.handleEvent(event).catch((e) => {
2736
+ log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
2737
+ });
2738
+ });
2715
2739
  log(`Watching ${collections2.length} collection(s): ${collections2.map((f) => f.name).join(", ")}`);
2716
2740
  };
2717
2741
  await rebuildWatcher(config.collections);
2718
- let reloadingCollections = false;
2742
+ let reloading = false;
2719
2743
  const pollTimer = setInterval(() => {
2720
- if (reloadingCollections)
2744
+ if (reloading)
2721
2745
  return;
2722
2746
  let nextConfig;
2723
2747
  try {
@@ -2728,26 +2752,61 @@ async function startForeground() {
2728
2752
  }
2729
2753
  ({ config: normalizedConfig, removedOverlappingCollections } = normalizeConfigCollections(nextConfig));
2730
2754
  maybeLogOverlapWarning(removedOverlappingCollections);
2731
- reloadingCollections = true;
2755
+ const coreChanged = normalizedConfig.server !== config.server || normalizedConfig.token !== config.token || normalizedConfig.username !== config.username;
2756
+ if (!coreChanged) {
2757
+ const { nextConfig: reconciledConfig, added, removed } = reconcileCollections(config, normalizedConfig);
2758
+ if (added.length === 0 && removed.length === 0)
2759
+ return;
2760
+ reloading = true;
2761
+ (async () => {
2762
+ try {
2763
+ log(`Collections changed: +${added.map((c) => c.name).join(", ") || "none"}, -${removed.map((c) => c.name).join(", ") || "none"}`);
2764
+ config = reconciledConfig;
2765
+ syncer.setCollections(reconciledConfig.collections);
2766
+ await rebuildWatcher(reconciledConfig.collections);
2767
+ for (const collection of removed) {
2768
+ await syncer.purgeCollection(collection);
2769
+ }
2770
+ for (const collection of added) {
2771
+ await syncer.syncCollection(collection);
2772
+ }
2773
+ } catch (e) {
2774
+ log(`Failed to reload collections: ${e.message}`);
2775
+ } finally {
2776
+ reloading = false;
2777
+ }
2778
+ })();
2779
+ return;
2780
+ }
2781
+ reloading = true;
2732
2782
  (async () => {
2733
2783
  try {
2734
- const { nextConfig: reconciledConfig, added, removed } = reconcileCollections(config, normalizedConfig);
2735
- if (added.length === 0 && removed.length === 0)
2736
- return;
2737
- log(`Collections changed: +${added.map((c) => c.name).join(", ") || "none"}, -${removed.map((c) => c.name).join(", ") || "none"}`);
2738
- config = reconciledConfig;
2739
- syncer.setCollections(reconciledConfig.collections);
2740
- await rebuildWatcher(reconciledConfig.collections);
2741
- for (const collection of removed) {
2742
- await syncer.purgeCollection(collection);
2743
- }
2744
- for (const collection of added) {
2745
- await syncer.syncCollection(collection);
2784
+ log("Config changed, reinitializing...");
2785
+ if (config.server !== normalizedConfig.server)
2786
+ log(` Server: ${config.server} -> ${normalizedConfig.server}`);
2787
+ if (config.username !== normalizedConfig.username)
2788
+ log(` Username: ${config.username} -> ${normalizedConfig.username}`);
2789
+ if (config.token !== normalizedConfig.token)
2790
+ log(` Token: updated`);
2791
+ syncer.stop();
2792
+ client = createClient(normalizedConfig.server, normalizedConfig.token);
2793
+ config = normalizedConfig;
2794
+ syncer = new Syncer({
2795
+ client,
2796
+ username: config.username,
2797
+ collections: config.collections,
2798
+ onLog: log
2799
+ });
2800
+ await rebuildWatcher(config.collections);
2801
+ if (config.collections.length > 0) {
2802
+ log("Running sync after reinitialize...");
2803
+ const { uploaded, skipped, deleted } = await syncer.initialSync();
2804
+ log(`Sync complete: ${uploaded} uploaded, ${skipped} skipped, ${deleted} deleted`);
2746
2805
  }
2747
2806
  } catch (e) {
2748
- log(`Failed to reload collections from config: ${e.message}`);
2807
+ log(`Failed to reinitialize: ${e.message}`);
2749
2808
  } finally {
2750
- reloadingCollections = false;
2809
+ reloading = false;
2751
2810
  }
2752
2811
  })();
2753
2812
  }, 1500);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedvault/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sv": "bin/sv.mjs"