@seedvault/cli 0.3.0 → 0.4.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/dist/sv.js +141 -84
- package/package.json +6 -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)
|
|
@@ -196,22 +200,48 @@ function createClient(serverUrl, token) {
|
|
|
196
200
|
return res.json();
|
|
197
201
|
},
|
|
198
202
|
async putFile(username, path, content) {
|
|
199
|
-
const res = await request("PUT", `/v1/
|
|
203
|
+
const res = await request("PUT", `/v1/files/${username}/${encodePath(path)}`, {
|
|
200
204
|
body: content,
|
|
201
205
|
contentType: "text/markdown"
|
|
202
206
|
});
|
|
203
207
|
return res.json();
|
|
204
208
|
},
|
|
205
209
|
async deleteFile(username, path) {
|
|
206
|
-
await request("DELETE", `/v1/
|
|
210
|
+
await request("DELETE", `/v1/files/${username}/${encodePath(path)}`);
|
|
207
211
|
},
|
|
208
212
|
async listFiles(username, prefix) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
213
|
+
const fullPrefix = prefix ? `${username}/${prefix}` : `${username}/`;
|
|
214
|
+
const qs = `?prefix=${encodeURIComponent(fullPrefix)}`;
|
|
215
|
+
const res = await request("GET", `/v1/files${qs}`);
|
|
216
|
+
const data = await res.json();
|
|
217
|
+
return {
|
|
218
|
+
files: data.files.map((f) => ({
|
|
219
|
+
...f,
|
|
220
|
+
path: f.path.startsWith(`${username}/`) ? f.path.slice(username.length + 1) : f.path
|
|
221
|
+
}))
|
|
222
|
+
};
|
|
212
223
|
},
|
|
213
224
|
async getFile(username, path) {
|
|
214
|
-
const
|
|
225
|
+
const fullPath = `${username}/${path}`;
|
|
226
|
+
const res = await request("POST", "/v1/sh", {
|
|
227
|
+
body: JSON.stringify({ cmd: `cat "${fullPath}"` }),
|
|
228
|
+
contentType: "application/json"
|
|
229
|
+
});
|
|
230
|
+
const exitCode = parseInt(res.headers.get("X-Exit-Code") || "0", 10);
|
|
231
|
+
if (exitCode !== 0) {
|
|
232
|
+
const stderr = decodeURIComponent(res.headers.get("X-Stderr") || "");
|
|
233
|
+
if (stderr.includes("No such file or directory")) {
|
|
234
|
+
throw new ApiError(404, "File not found");
|
|
235
|
+
}
|
|
236
|
+
throw new ApiError(500, stderr || `cat exited with code ${exitCode}`);
|
|
237
|
+
}
|
|
238
|
+
return res.text();
|
|
239
|
+
},
|
|
240
|
+
async sh(cmd) {
|
|
241
|
+
const res = await request("POST", "/v1/sh", {
|
|
242
|
+
body: JSON.stringify({ cmd }),
|
|
243
|
+
contentType: "application/json"
|
|
244
|
+
});
|
|
215
245
|
return res.text();
|
|
216
246
|
},
|
|
217
247
|
async health() {
|
|
@@ -231,15 +261,12 @@ async function init(args) {
|
|
|
231
261
|
}
|
|
232
262
|
if (flags.server && flags.token) {
|
|
233
263
|
const client = createClient(flags.server, flags.token);
|
|
264
|
+
let username;
|
|
234
265
|
try {
|
|
235
|
-
await client.
|
|
266
|
+
const me = await client.me();
|
|
267
|
+
username = me.username;
|
|
236
268
|
} catch {
|
|
237
|
-
console.error(`Could not
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
const username = flags["username"] || "";
|
|
241
|
-
if (!username) {
|
|
242
|
-
console.error("When using --token, also pass --username");
|
|
269
|
+
console.error(`Could not authenticate with server at ${flags.server}`);
|
|
243
270
|
process.exit(1);
|
|
244
271
|
}
|
|
245
272
|
const config = {
|
|
@@ -303,12 +330,20 @@ async function init(args) {
|
|
|
303
330
|
}
|
|
304
331
|
const hasToken = await rl.question("Do you already have a token? (y/N): ");
|
|
305
332
|
if (hasToken.toLowerCase() === "y") {
|
|
306
|
-
const token = await rl.question("Token: ");
|
|
307
|
-
const
|
|
308
|
-
|
|
333
|
+
const token = (await rl.question("Token: ")).trim();
|
|
334
|
+
const authedClient = createClient(server, token);
|
|
335
|
+
let username;
|
|
336
|
+
try {
|
|
337
|
+
const me = await authedClient.me();
|
|
338
|
+
username = me.username;
|
|
339
|
+
} catch {
|
|
340
|
+
console.error(" Token is invalid or server rejected it.");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const config = { server, token, username, collections: [] };
|
|
309
344
|
saveConfig(config);
|
|
310
345
|
console.log(`
|
|
311
|
-
Seedvault configured
|
|
346
|
+
Seedvault configured as '${username}'`);
|
|
312
347
|
} else {
|
|
313
348
|
const name = await rl.question("Username (e.g. your-name-notes): ");
|
|
314
349
|
const invite = await rl.question("Invite code (leave blank if first user): ");
|
|
@@ -333,6 +368,7 @@ Next steps:`);
|
|
|
333
368
|
} finally {
|
|
334
369
|
rl.close();
|
|
335
370
|
}
|
|
371
|
+
process.exit(0);
|
|
336
372
|
}
|
|
337
373
|
function parseFlags(args) {
|
|
338
374
|
const flags = {};
|
|
@@ -2126,6 +2162,8 @@ class RetryQueue {
|
|
|
2126
2162
|
}
|
|
2127
2163
|
|
|
2128
2164
|
// src/daemon/syncer.ts
|
|
2165
|
+
var SYNC_CONCURRENCY = 10;
|
|
2166
|
+
|
|
2129
2167
|
class Syncer {
|
|
2130
2168
|
client;
|
|
2131
2169
|
username;
|
|
@@ -2167,6 +2205,7 @@ class Syncer {
|
|
|
2167
2205
|
}
|
|
2168
2206
|
const localFiles = await walkMd(collection.path);
|
|
2169
2207
|
const localServerPaths = new Set;
|
|
2208
|
+
const toUpload = [];
|
|
2170
2209
|
for (const localFile of localFiles) {
|
|
2171
2210
|
const relPath = toPosixPath(relative5(collection.path, localFile.path));
|
|
2172
2211
|
const serverPath = `${collection.name}/${relPath}`;
|
|
@@ -2181,22 +2220,24 @@ class Syncer {
|
|
|
2181
2220
|
}
|
|
2182
2221
|
}
|
|
2183
2222
|
const content = await readFile(localFile.path, "utf-8");
|
|
2223
|
+
toUpload.push({ serverPath, content });
|
|
2224
|
+
}
|
|
2225
|
+
await pooled(toUpload, SYNC_CONCURRENCY, async (item) => {
|
|
2184
2226
|
try {
|
|
2185
|
-
await this.client.putFile(this.username, serverPath, content);
|
|
2227
|
+
await this.client.putFile(this.username, item.serverPath, item.content);
|
|
2186
2228
|
uploaded++;
|
|
2187
2229
|
} catch {
|
|
2188
2230
|
this.queue.enqueue({
|
|
2189
2231
|
type: "put",
|
|
2190
2232
|
username: this.username,
|
|
2191
|
-
serverPath,
|
|
2192
|
-
content,
|
|
2233
|
+
serverPath: item.serverPath,
|
|
2234
|
+
content: item.content,
|
|
2193
2235
|
queuedAt: new Date().toISOString()
|
|
2194
2236
|
});
|
|
2195
2237
|
}
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
continue;
|
|
2238
|
+
});
|
|
2239
|
+
const toDelete = serverFiles.filter((f) => !localServerPaths.has(f.path));
|
|
2240
|
+
await pooled(toDelete, SYNC_CONCURRENCY, async (f) => {
|
|
2200
2241
|
try {
|
|
2201
2242
|
await this.client.deleteFile(this.username, f.path);
|
|
2202
2243
|
deleted++;
|
|
@@ -2209,7 +2250,7 @@ class Syncer {
|
|
|
2209
2250
|
queuedAt: new Date().toISOString()
|
|
2210
2251
|
});
|
|
2211
2252
|
}
|
|
2212
|
-
}
|
|
2253
|
+
});
|
|
2213
2254
|
this.log(` '${collection.name}': ${uploaded} uploaded, ${skipped} up-to-date, ${deleted} deleted`);
|
|
2214
2255
|
} catch (e) {
|
|
2215
2256
|
this.log(` '${collection.name}': sync failed (${e.message})`);
|
|
@@ -2222,7 +2263,7 @@ class Syncer {
|
|
|
2222
2263
|
this.log(`Removing '${collection.name}' files from server...`);
|
|
2223
2264
|
try {
|
|
2224
2265
|
const { files: serverFiles } = await this.client.listFiles(this.username, collection.name + "/");
|
|
2225
|
-
|
|
2266
|
+
await pooled(serverFiles, SYNC_CONCURRENCY, async (f) => {
|
|
2226
2267
|
try {
|
|
2227
2268
|
await this.client.deleteFile(this.username, f.path);
|
|
2228
2269
|
deleted++;
|
|
@@ -2236,7 +2277,7 @@ class Syncer {
|
|
|
2236
2277
|
});
|
|
2237
2278
|
queued++;
|
|
2238
2279
|
}
|
|
2239
|
-
}
|
|
2280
|
+
});
|
|
2240
2281
|
this.log(` '${collection.name}': ${deleted} deleted, ${queued} queued`);
|
|
2241
2282
|
} catch (e) {
|
|
2242
2283
|
this.log(` '${collection.name}': remove failed (${e.message})`);
|
|
@@ -2280,6 +2321,16 @@ async function walkMd(dir) {
|
|
|
2280
2321
|
await walkDirRecursive(dir, results);
|
|
2281
2322
|
return results;
|
|
2282
2323
|
}
|
|
2324
|
+
async function pooled(items, concurrency, fn) {
|
|
2325
|
+
let i = 0;
|
|
2326
|
+
async function worker() {
|
|
2327
|
+
while (i < items.length) {
|
|
2328
|
+
const idx = i++;
|
|
2329
|
+
await fn(items[idx]);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
2333
|
+
}
|
|
2283
2334
|
async function walkDirRecursive(dir, results) {
|
|
2284
2335
|
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2285
2336
|
for (const entry of entries) {
|
|
@@ -2646,7 +2697,7 @@ async function startForeground() {
|
|
|
2646
2697
|
if (removedOverlappingCollections.length > 0) {
|
|
2647
2698
|
config = normalizedConfig;
|
|
2648
2699
|
}
|
|
2649
|
-
|
|
2700
|
+
let client = createClient(config.server, config.token);
|
|
2650
2701
|
try {
|
|
2651
2702
|
await client.health();
|
|
2652
2703
|
} catch {
|
|
@@ -2680,7 +2731,7 @@ async function startForeground() {
|
|
|
2680
2731
|
log(` Collections: ${config.collections.map((f) => f.name).join(", ")}`);
|
|
2681
2732
|
}
|
|
2682
2733
|
writeFileSync2(getPidPath(), String(process.pid));
|
|
2683
|
-
|
|
2734
|
+
let syncer = new Syncer({
|
|
2684
2735
|
client,
|
|
2685
2736
|
username: config.username,
|
|
2686
2737
|
collections: config.collections,
|
|
@@ -2696,11 +2747,6 @@ async function startForeground() {
|
|
|
2696
2747
|
log("Will continue watching for changes...");
|
|
2697
2748
|
}
|
|
2698
2749
|
}
|
|
2699
|
-
const onWatcherEvent = (event) => {
|
|
2700
|
-
syncer.handleEvent(event).catch((e) => {
|
|
2701
|
-
log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
|
|
2702
|
-
});
|
|
2703
|
-
};
|
|
2704
2750
|
let watcher = null;
|
|
2705
2751
|
const rebuildWatcher = async (collections2) => {
|
|
2706
2752
|
if (watcher) {
|
|
@@ -2711,13 +2757,17 @@ async function startForeground() {
|
|
|
2711
2757
|
log("No collections configured. Daemon idle.");
|
|
2712
2758
|
return;
|
|
2713
2759
|
}
|
|
2714
|
-
watcher = createWatcher(collections2,
|
|
2760
|
+
watcher = createWatcher(collections2, (event) => {
|
|
2761
|
+
syncer.handleEvent(event).catch((e) => {
|
|
2762
|
+
log(`Error handling ${event.type} for ${event.serverPath}: ${e.message}`);
|
|
2763
|
+
});
|
|
2764
|
+
});
|
|
2715
2765
|
log(`Watching ${collections2.length} collection(s): ${collections2.map((f) => f.name).join(", ")}`);
|
|
2716
2766
|
};
|
|
2717
2767
|
await rebuildWatcher(config.collections);
|
|
2718
|
-
let
|
|
2768
|
+
let reloading = false;
|
|
2719
2769
|
const pollTimer = setInterval(() => {
|
|
2720
|
-
if (
|
|
2770
|
+
if (reloading)
|
|
2721
2771
|
return;
|
|
2722
2772
|
let nextConfig;
|
|
2723
2773
|
try {
|
|
@@ -2728,26 +2778,61 @@ async function startForeground() {
|
|
|
2728
2778
|
}
|
|
2729
2779
|
({ config: normalizedConfig, removedOverlappingCollections } = normalizeConfigCollections(nextConfig));
|
|
2730
2780
|
maybeLogOverlapWarning(removedOverlappingCollections);
|
|
2731
|
-
|
|
2781
|
+
const coreChanged = normalizedConfig.server !== config.server || normalizedConfig.token !== config.token || normalizedConfig.username !== config.username;
|
|
2782
|
+
if (!coreChanged) {
|
|
2783
|
+
const { nextConfig: reconciledConfig, added, removed } = reconcileCollections(config, normalizedConfig);
|
|
2784
|
+
if (added.length === 0 && removed.length === 0)
|
|
2785
|
+
return;
|
|
2786
|
+
reloading = true;
|
|
2787
|
+
(async () => {
|
|
2788
|
+
try {
|
|
2789
|
+
log(`Collections changed: +${added.map((c) => c.name).join(", ") || "none"}, -${removed.map((c) => c.name).join(", ") || "none"}`);
|
|
2790
|
+
config = reconciledConfig;
|
|
2791
|
+
syncer.setCollections(reconciledConfig.collections);
|
|
2792
|
+
await rebuildWatcher(reconciledConfig.collections);
|
|
2793
|
+
for (const collection of removed) {
|
|
2794
|
+
await syncer.purgeCollection(collection);
|
|
2795
|
+
}
|
|
2796
|
+
for (const collection of added) {
|
|
2797
|
+
await syncer.syncCollection(collection);
|
|
2798
|
+
}
|
|
2799
|
+
} catch (e) {
|
|
2800
|
+
log(`Failed to reload collections: ${e.message}`);
|
|
2801
|
+
} finally {
|
|
2802
|
+
reloading = false;
|
|
2803
|
+
}
|
|
2804
|
+
})();
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
reloading = true;
|
|
2732
2808
|
(async () => {
|
|
2733
2809
|
try {
|
|
2734
|
-
|
|
2735
|
-
if (
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2810
|
+
log("Config changed, reinitializing...");
|
|
2811
|
+
if (config.server !== normalizedConfig.server)
|
|
2812
|
+
log(` Server: ${config.server} -> ${normalizedConfig.server}`);
|
|
2813
|
+
if (config.username !== normalizedConfig.username)
|
|
2814
|
+
log(` Username: ${config.username} -> ${normalizedConfig.username}`);
|
|
2815
|
+
if (config.token !== normalizedConfig.token)
|
|
2816
|
+
log(` Token: updated`);
|
|
2817
|
+
syncer.stop();
|
|
2818
|
+
client = createClient(normalizedConfig.server, normalizedConfig.token);
|
|
2819
|
+
config = normalizedConfig;
|
|
2820
|
+
syncer = new Syncer({
|
|
2821
|
+
client,
|
|
2822
|
+
username: config.username,
|
|
2823
|
+
collections: config.collections,
|
|
2824
|
+
onLog: log
|
|
2825
|
+
});
|
|
2826
|
+
await rebuildWatcher(config.collections);
|
|
2827
|
+
if (config.collections.length > 0) {
|
|
2828
|
+
log("Running sync after reinitialize...");
|
|
2829
|
+
const { uploaded, skipped, deleted } = await syncer.initialSync();
|
|
2830
|
+
log(`Sync complete: ${uploaded} uploaded, ${skipped} skipped, ${deleted} deleted`);
|
|
2746
2831
|
}
|
|
2747
2832
|
} catch (e) {
|
|
2748
|
-
log(`Failed to
|
|
2833
|
+
log(`Failed to reinitialize: ${e.message}`);
|
|
2749
2834
|
} finally {
|
|
2750
|
-
|
|
2835
|
+
reloading = false;
|
|
2751
2836
|
}
|
|
2752
2837
|
})();
|
|
2753
2838
|
}, 1500);
|
|
@@ -2849,27 +2934,8 @@ async function status() {
|
|
|
2849
2934
|
async function ls(args) {
|
|
2850
2935
|
const config = loadConfig();
|
|
2851
2936
|
const client = createClient(config.server, config.token);
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
if (files.length === 0) {
|
|
2855
|
-
console.log(prefix ? `No files matching '${prefix}'.` : "No files in your contributor.");
|
|
2856
|
-
return;
|
|
2857
|
-
}
|
|
2858
|
-
const maxPath = Math.max(...files.map((f) => f.path.length));
|
|
2859
|
-
for (const f of files) {
|
|
2860
|
-
const size = formatSize(f.size);
|
|
2861
|
-
const date = new Date(f.modifiedAt).toLocaleString();
|
|
2862
|
-
console.log(` ${f.path.padEnd(maxPath + 2)} ${size.padStart(8)} ${date}`);
|
|
2863
|
-
}
|
|
2864
|
-
console.log(`
|
|
2865
|
-
${files.length} file(s)`);
|
|
2866
|
-
}
|
|
2867
|
-
function formatSize(bytes) {
|
|
2868
|
-
if (bytes < 1024)
|
|
2869
|
-
return `${bytes} B`;
|
|
2870
|
-
if (bytes < 1024 * 1024)
|
|
2871
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2872
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2937
|
+
const output = await client.sh(`ls ${args.join(" ")}`);
|
|
2938
|
+
process.stdout.write(output);
|
|
2873
2939
|
}
|
|
2874
2940
|
|
|
2875
2941
|
// src/commands/cat.ts
|
|
@@ -2878,19 +2944,10 @@ async function cat(args) {
|
|
|
2878
2944
|
console.error("Usage: sv cat <path>");
|
|
2879
2945
|
process.exit(1);
|
|
2880
2946
|
}
|
|
2881
|
-
const filePath = args[0];
|
|
2882
2947
|
const config = loadConfig();
|
|
2883
2948
|
const client = createClient(config.server, config.token);
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
process.stdout.write(content);
|
|
2887
|
-
} catch (e) {
|
|
2888
|
-
if (e instanceof ApiError && e.status === 404) {
|
|
2889
|
-
console.error(`File not found: ${filePath}`);
|
|
2890
|
-
process.exit(1);
|
|
2891
|
-
}
|
|
2892
|
-
throw e;
|
|
2893
|
-
}
|
|
2949
|
+
const output = await client.sh(`cat ${args.join(" ")}`);
|
|
2950
|
+
process.stdout.write(output);
|
|
2894
2951
|
}
|
|
2895
2952
|
|
|
2896
2953
|
// src/commands/contributors.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seedvault/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sv": "bin/sv.mjs"
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"bin"
|
|
17
17
|
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/collaborator-ai/seedvault.git",
|
|
21
|
+
"directory": "cli"
|
|
22
|
+
},
|
|
18
23
|
"dependencies": {
|
|
19
24
|
"chokidar": "^4"
|
|
20
25
|
},
|