@seedvault/cli 0.1.0 → 0.1.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 +2705 -0
- package/package.json +8 -3
- package/src/client.ts +0 -164
- package/src/commands/add.ts +0 -52
- package/src/commands/cat.ts +0 -29
- package/src/commands/collections.ts +0 -24
- package/src/commands/contributors.ts +0 -28
- package/src/commands/init.ts +0 -153
- package/src/commands/invite.ts +0 -26
- package/src/commands/ls.ts +0 -37
- package/src/commands/remove.ts +0 -25
- package/src/commands/start.ts +0 -258
- package/src/commands/status.ts +0 -63
- package/src/commands/stop.ts +0 -51
- package/src/config.ts +0 -182
- package/src/daemon/queue.ts +0 -107
- package/src/daemon/syncer.ts +0 -254
- package/src/daemon/watcher.ts +0 -71
- package/src/index.ts +0 -93
- package/tsconfig.json +0 -19
package/src/daemon/syncer.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { readdir, stat, readFile } from "fs/promises";
|
|
2
|
-
import { join, relative } from "path";
|
|
3
|
-
import type { SeedvaultClient } from "../client.js";
|
|
4
|
-
import type { CollectionConfig } from "../config.js";
|
|
5
|
-
import type { FileEvent } from "./watcher.js";
|
|
6
|
-
import { RetryQueue } from "./queue.js";
|
|
7
|
-
|
|
8
|
-
export interface SyncerOptions {
|
|
9
|
-
client: SeedvaultClient;
|
|
10
|
-
contributorId: string;
|
|
11
|
-
collections: CollectionConfig[];
|
|
12
|
-
onLog: (msg: string) => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class Syncer {
|
|
16
|
-
private client: SeedvaultClient;
|
|
17
|
-
private contributorId: string;
|
|
18
|
-
private collections: CollectionConfig[];
|
|
19
|
-
private queue: RetryQueue;
|
|
20
|
-
private log: (msg: string) => void;
|
|
21
|
-
|
|
22
|
-
constructor(opts: SyncerOptions) {
|
|
23
|
-
this.client = opts.client;
|
|
24
|
-
this.contributorId = opts.contributorId;
|
|
25
|
-
this.collections = opts.collections;
|
|
26
|
-
this.log = opts.onLog;
|
|
27
|
-
this.queue = new RetryQueue(opts.client, opts.onLog);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Update the active collections set used for whole-collection syncs. */
|
|
31
|
-
setCollections(collections: CollectionConfig[]): void {
|
|
32
|
-
this.collections = [...collections];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Initial sync: compare local files against what's on the server,
|
|
37
|
-
* PUT anything that's newer or missing, and DELETE files that
|
|
38
|
-
* exist on server but no longer exist locally.
|
|
39
|
-
*/
|
|
40
|
-
async initialSync(): Promise<{ uploaded: number; skipped: number; deleted: number }> {
|
|
41
|
-
let uploaded = 0;
|
|
42
|
-
let skipped = 0;
|
|
43
|
-
let deleted = 0;
|
|
44
|
-
|
|
45
|
-
for (const collection of [...this.collections]) {
|
|
46
|
-
const result = await this.syncCollection(collection);
|
|
47
|
-
uploaded += result.uploaded;
|
|
48
|
-
skipped += result.skipped;
|
|
49
|
-
deleted += result.deleted;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { uploaded, skipped, deleted };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Sync a single collection.
|
|
57
|
-
*/
|
|
58
|
-
async syncCollection(collection: CollectionConfig): Promise<{ uploaded: number; skipped: number; deleted: number }> {
|
|
59
|
-
let uploaded = 0;
|
|
60
|
-
let skipped = 0;
|
|
61
|
-
let deleted = 0;
|
|
62
|
-
|
|
63
|
-
this.log(`Syncing '${collection.name}' (${collection.path})...`);
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
// Get server file listing for this collection's prefix
|
|
67
|
-
const { files: serverFiles } = await this.client.listFiles(
|
|
68
|
-
this.contributorId,
|
|
69
|
-
collection.name + "/"
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Build a map of server files by path -> modifiedAt
|
|
73
|
-
const serverMap = new Map<string, string>();
|
|
74
|
-
for (const f of serverFiles) {
|
|
75
|
-
serverMap.set(f.path, f.modifiedAt);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Walk local directory for .md files
|
|
79
|
-
const localFiles = await walkMd(collection.path);
|
|
80
|
-
const localServerPaths = new Set<string>();
|
|
81
|
-
|
|
82
|
-
for (const localFile of localFiles) {
|
|
83
|
-
const relPath = toPosixPath(relative(collection.path, localFile.path));
|
|
84
|
-
const serverPath = `${collection.name}/${relPath}`;
|
|
85
|
-
localServerPaths.add(serverPath);
|
|
86
|
-
|
|
87
|
-
const serverMod = serverMap.get(serverPath);
|
|
88
|
-
if (serverMod) {
|
|
89
|
-
// File exists on server — compare mtime
|
|
90
|
-
const serverDate = new Date(serverMod).getTime();
|
|
91
|
-
const localDate = localFile.mtimeMs;
|
|
92
|
-
if (localDate <= serverDate) {
|
|
93
|
-
skipped++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Upload
|
|
99
|
-
const content = await readFile(localFile.path, "utf-8");
|
|
100
|
-
try {
|
|
101
|
-
await this.client.putFile(this.contributorId, serverPath, content);
|
|
102
|
-
uploaded++;
|
|
103
|
-
} catch {
|
|
104
|
-
// If server unreachable, queue it
|
|
105
|
-
this.queue.enqueue({
|
|
106
|
-
type: "put",
|
|
107
|
-
contributorId: this.contributorId,
|
|
108
|
-
serverPath,
|
|
109
|
-
content,
|
|
110
|
-
queuedAt: new Date().toISOString(),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Delete server files that no longer exist locally
|
|
116
|
-
for (const f of serverFiles) {
|
|
117
|
-
if (localServerPaths.has(f.path)) continue;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
await this.client.deleteFile(this.contributorId, f.path);
|
|
121
|
-
deleted++;
|
|
122
|
-
} catch {
|
|
123
|
-
// If server unreachable, queue it
|
|
124
|
-
this.queue.enqueue({
|
|
125
|
-
type: "delete",
|
|
126
|
-
contributorId: this.contributorId,
|
|
127
|
-
serverPath: f.path,
|
|
128
|
-
content: null,
|
|
129
|
-
queuedAt: new Date().toISOString(),
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.log(
|
|
135
|
-
` '${collection.name}': ${uploaded} uploaded, ${skipped} up-to-date, ${deleted} deleted`
|
|
136
|
-
);
|
|
137
|
-
} catch (e: unknown) {
|
|
138
|
-
this.log(` '${collection.name}': sync failed (${(e as Error).message})`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { uploaded, skipped, deleted };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Remove all remote files under a collection prefix.
|
|
146
|
-
* Used when a collection is removed from config while the daemon is running.
|
|
147
|
-
*/
|
|
148
|
-
async purgeCollection(collection: CollectionConfig): Promise<{ deleted: number; queued: number }> {
|
|
149
|
-
let deleted = 0;
|
|
150
|
-
let queued = 0;
|
|
151
|
-
|
|
152
|
-
this.log(`Removing '${collection.name}' files from server...`);
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const { files: serverFiles } = await this.client.listFiles(
|
|
156
|
-
this.contributorId,
|
|
157
|
-
collection.name + "/"
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
for (const f of serverFiles) {
|
|
161
|
-
try {
|
|
162
|
-
await this.client.deleteFile(this.contributorId, f.path);
|
|
163
|
-
deleted++;
|
|
164
|
-
} catch {
|
|
165
|
-
this.queue.enqueue({
|
|
166
|
-
type: "delete",
|
|
167
|
-
contributorId: this.contributorId,
|
|
168
|
-
serverPath: f.path,
|
|
169
|
-
content: null,
|
|
170
|
-
queuedAt: new Date().toISOString(),
|
|
171
|
-
});
|
|
172
|
-
queued++;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
this.log(` '${collection.name}': ${deleted} deleted, ${queued} queued`);
|
|
177
|
-
} catch (e: unknown) {
|
|
178
|
-
this.log(` '${collection.name}': remove failed (${(e as Error).message})`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { deleted, queued };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Handle a file event from the watcher.
|
|
186
|
-
*/
|
|
187
|
-
async handleEvent(event: FileEvent): Promise<void> {
|
|
188
|
-
if (event.type === "add" || event.type === "change") {
|
|
189
|
-
const content = await readFile(event.localPath, "utf-8");
|
|
190
|
-
this.log(`PUT ${event.serverPath} (${content.length} bytes)`);
|
|
191
|
-
this.queue.enqueue({
|
|
192
|
-
type: "put",
|
|
193
|
-
contributorId: this.contributorId,
|
|
194
|
-
serverPath: event.serverPath,
|
|
195
|
-
content,
|
|
196
|
-
queuedAt: new Date().toISOString(),
|
|
197
|
-
});
|
|
198
|
-
} else if (event.type === "unlink") {
|
|
199
|
-
this.log(`DELETE ${event.serverPath}`);
|
|
200
|
-
this.queue.enqueue({
|
|
201
|
-
type: "delete",
|
|
202
|
-
contributorId: this.contributorId,
|
|
203
|
-
serverPath: event.serverPath,
|
|
204
|
-
content: null,
|
|
205
|
-
queuedAt: new Date().toISOString(),
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Stop retry timers. Pending ops remain in memory for process lifetime only. */
|
|
211
|
-
stop(): void {
|
|
212
|
-
this.queue.stop();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Number of pending queued operations */
|
|
216
|
-
get pendingOps(): number {
|
|
217
|
-
return this.queue.pending;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// --- Helpers ---
|
|
222
|
-
|
|
223
|
-
interface LocalFile {
|
|
224
|
-
path: string;
|
|
225
|
-
mtimeMs: number;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function toPosixPath(path: string): string {
|
|
229
|
-
return path.split("\\").join("/");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function walkMd(dir: string): Promise<LocalFile[]> {
|
|
233
|
-
const results: LocalFile[] = [];
|
|
234
|
-
await walkDirRecursive(dir, results);
|
|
235
|
-
return results;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function walkDirRecursive(dir: string, results: LocalFile[]): Promise<void> {
|
|
239
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
240
|
-
|
|
241
|
-
for (const entry of entries) {
|
|
242
|
-
// Skip hidden dirs and node_modules
|
|
243
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
244
|
-
|
|
245
|
-
const full = join(dir, entry.name);
|
|
246
|
-
|
|
247
|
-
if (entry.isDirectory()) {
|
|
248
|
-
await walkDirRecursive(full, results);
|
|
249
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
250
|
-
const s = await stat(full);
|
|
251
|
-
results.push({ path: full, mtimeMs: s.mtimeMs });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
package/src/daemon/watcher.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { watch, type FSWatcher } from "chokidar";
|
|
2
|
-
import { relative } from "path";
|
|
3
|
-
import type { CollectionConfig } from "../config.js";
|
|
4
|
-
|
|
5
|
-
export type FileEvent =
|
|
6
|
-
| { type: "add" | "change"; serverPath: string; localPath: string }
|
|
7
|
-
| { type: "unlink"; serverPath: string; localPath: string };
|
|
8
|
-
|
|
9
|
-
export type EventHandler = (event: FileEvent) => void;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Create a chokidar watcher for a set of configured collections.
|
|
13
|
-
* Maps local file events to server-relative paths using collection names.
|
|
14
|
-
*/
|
|
15
|
-
export function createWatcher(
|
|
16
|
-
collections: CollectionConfig[],
|
|
17
|
-
onEvent: EventHandler
|
|
18
|
-
): FSWatcher {
|
|
19
|
-
// Build the paths to watch
|
|
20
|
-
const paths = collections.map((f) => f.path);
|
|
21
|
-
|
|
22
|
-
const watcher = watch(paths, {
|
|
23
|
-
ignored: [
|
|
24
|
-
/(^|[/\\])\./, // dotfiles / dotdirs (.git, .DS_Store, etc.)
|
|
25
|
-
"**/node_modules/**",
|
|
26
|
-
"**/*.tmp.*",
|
|
27
|
-
],
|
|
28
|
-
persistent: true,
|
|
29
|
-
ignoreInitial: true, // we handle initial sync separately
|
|
30
|
-
awaitWriteFinish: {
|
|
31
|
-
stabilityThreshold: 300,
|
|
32
|
-
pollInterval: 100,
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Build a lookup: absolute collection path -> collection name
|
|
37
|
-
const collectionMap = new Map<string, string>();
|
|
38
|
-
for (const f of collections) {
|
|
39
|
-
collectionMap.set(f.path, f.name);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function toServerPath(localPath: string): string | null {
|
|
43
|
-
// Only markdown files
|
|
44
|
-
if (!localPath.endsWith(".md")) return null;
|
|
45
|
-
|
|
46
|
-
for (const [collectionPath, name] of collectionMap) {
|
|
47
|
-
if (localPath.startsWith(collectionPath + "/") || localPath === collectionPath) {
|
|
48
|
-
const rel = relative(collectionPath, localPath);
|
|
49
|
-
return `${name}/${rel}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
watcher.on("add", (path) => {
|
|
56
|
-
const sp = toServerPath(path);
|
|
57
|
-
if (sp) onEvent({ type: "add", serverPath: sp, localPath: path });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
watcher.on("change", (path) => {
|
|
61
|
-
const sp = toServerPath(path);
|
|
62
|
-
if (sp) onEvent({ type: "change", serverPath: sp, localPath: path });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
watcher.on("unlink", (path) => {
|
|
66
|
-
const sp = toServerPath(path);
|
|
67
|
-
if (sp) onEvent({ type: "unlink", serverPath: sp, localPath: path });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return watcher;
|
|
71
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { init } from "./commands/init.js";
|
|
4
|
-
import { add } from "./commands/add.js";
|
|
5
|
-
import { remove } from "./commands/remove.js";
|
|
6
|
-
import { collections } from "./commands/collections.js";
|
|
7
|
-
import { start } from "./commands/start.js";
|
|
8
|
-
import { stop } from "./commands/stop.js";
|
|
9
|
-
import { status } from "./commands/status.js";
|
|
10
|
-
import { ls } from "./commands/ls.js";
|
|
11
|
-
import { cat } from "./commands/cat.js";
|
|
12
|
-
import { contributors } from "./commands/contributors.js";
|
|
13
|
-
import { invite } from "./commands/invite.js";
|
|
14
|
-
|
|
15
|
-
const USAGE = `
|
|
16
|
-
Seedvault CLI
|
|
17
|
-
|
|
18
|
-
Usage: sv <command> [options]
|
|
19
|
-
|
|
20
|
-
Setup:
|
|
21
|
-
init Interactive first-time setup
|
|
22
|
-
init --server URL --token T Non-interactive (existing token)
|
|
23
|
-
init --server URL --name N Non-interactive (signup)
|
|
24
|
-
|
|
25
|
-
Collections:
|
|
26
|
-
add <path> [--name N] Add a collection path
|
|
27
|
-
remove <name> Remove a collection by name
|
|
28
|
-
collections List configured collections
|
|
29
|
-
|
|
30
|
-
Daemon:
|
|
31
|
-
start Start syncing (foreground)
|
|
32
|
-
start -d Start syncing (background)
|
|
33
|
-
stop Stop the daemon
|
|
34
|
-
status Show sync status
|
|
35
|
-
|
|
36
|
-
Files:
|
|
37
|
-
ls [prefix] List files in your contributor
|
|
38
|
-
cat <path> Read a file from the server
|
|
39
|
-
|
|
40
|
-
Vault:
|
|
41
|
-
contributors List all contributors
|
|
42
|
-
invite Generate an invite code (operator only)
|
|
43
|
-
`.trim();
|
|
44
|
-
|
|
45
|
-
async function main(): Promise<void> {
|
|
46
|
-
const [cmd, ...args] = process.argv.slice(2);
|
|
47
|
-
|
|
48
|
-
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
49
|
-
console.log(USAGE);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (cmd === "--version" || cmd === "-v") {
|
|
54
|
-
console.log("0.1.0");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
switch (cmd) {
|
|
60
|
-
case "init":
|
|
61
|
-
return await init(args);
|
|
62
|
-
case "add":
|
|
63
|
-
return await add(args);
|
|
64
|
-
case "remove":
|
|
65
|
-
return await remove(args);
|
|
66
|
-
case "collections":
|
|
67
|
-
return await collections();
|
|
68
|
-
case "start":
|
|
69
|
-
return await start(args);
|
|
70
|
-
case "stop":
|
|
71
|
-
return await stop();
|
|
72
|
-
case "status":
|
|
73
|
-
return await status();
|
|
74
|
-
case "ls":
|
|
75
|
-
return await ls(args);
|
|
76
|
-
case "cat":
|
|
77
|
-
return await cat(args);
|
|
78
|
-
case "contributors":
|
|
79
|
-
return await contributors();
|
|
80
|
-
case "invite":
|
|
81
|
-
return await invite();
|
|
82
|
-
default:
|
|
83
|
-
console.error(`Unknown command: ${cmd}\n`);
|
|
84
|
-
console.log(USAGE);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
} catch (e: unknown) {
|
|
88
|
-
console.error(`Error: ${(e as Error).message}`);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
main();
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ES2022",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"resolveJsonModule": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "./dist",
|
|
14
|
-
"rootDir": "./src",
|
|
15
|
-
"types": ["bun"]
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*"],
|
|
18
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|