@syncast/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/index.js CHANGED
@@ -1,57 +1,760 @@
1
+ import { CHUNK_SIZE, DEFAULT_HOST, DEFAULT_PORT, ErrorCodes, FILE_CHANGE_DEBOUNCE_MS, FILE_STABLE_CHECK_INTERVAL_MS, PROTOCOL_VERSION, createConnectedMessage, createErrorMessage, createFileConfig, createFolderConfig, getFileConfigPath, getFilename, getFolderConfigPath, isConfigFile, isSupportedFile, parseFileConfig, parseFolderConfig, toRelativePath } from "local-sync-protocol";
2
+ import { WebSocketServer } from "ws";
3
+ import { mkdir, readFile, readdir, stat as promises_stat, writeFile } from "node:fs/promises";
4
+ import { dirname as external_node_path_dirname, join } from "node:path";
5
+ import { exec } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { watch } from "chokidar";
1
8
  import { Command } from "commander";
2
- async function createServer(options = {}) {
3
- const { port = 3456, host = "localhost" } = options;
4
- return {
5
- async start () {
6
- console.log(`🚀 Syncast server starting on ${host}:${port}`);
7
- },
8
- async stop () {
9
- console.log("🛑 Syncast server stopped");
9
+ class FileTransferManager {
10
+ rootPath;
11
+ events;
12
+ pendingReceives = new Map();
13
+ activeSends = new Set();
14
+ constructor(rootPath, events){
15
+ this.rootPath = rootPath;
16
+ this.events = events;
17
+ }
18
+ async sendFile(requestId, relativePath) {
19
+ const absolutePath = join(this.rootPath, relativePath);
20
+ if (this.activeSends.has(requestId)) return;
21
+ this.activeSends.add(requestId);
22
+ try {
23
+ const stat = await promises_stat(absolutePath);
24
+ if (!stat.isFile()) return void this.events.onError(requestId, ErrorCodes.FILE_NOT_FOUND, `Not a file: ${relativePath}`);
25
+ const fileBuffer = await readFile(absolutePath);
26
+ const totalChunks = Math.ceil(fileBuffer.length / CHUNK_SIZE);
27
+ console.log(`[FileTransfer] Sending file: ${relativePath} (${fileBuffer.length} bytes, ${totalChunks} chunks)`);
28
+ for(let i = 0; i < totalChunks; i++){
29
+ if (!this.activeSends.has(requestId)) return void console.log(`[FileTransfer] Send cancelled: ${relativePath}`);
30
+ const start = i * CHUNK_SIZE;
31
+ const end = Math.min(start + CHUNK_SIZE, fileBuffer.length);
32
+ const chunk = fileBuffer.subarray(start, end);
33
+ const base64Data = chunk.toString("base64");
34
+ this.events.onChunk(requestId, i, totalChunks, base64Data);
35
+ if (i < totalChunks - 1) await sleep(1);
36
+ }
37
+ this.events.onComplete(requestId, relativePath);
38
+ console.log(`[FileTransfer] File sent: ${relativePath}`);
39
+ } catch (error) {
40
+ console.error(`[FileTransfer] Failed to send file: ${relativePath}`, error);
41
+ this.events.onError(requestId, ErrorCodes.FILE_NOT_FOUND, `Failed to read file: ${relativePath}`);
42
+ } finally{
43
+ this.activeSends.delete(requestId);
10
44
  }
11
- };
45
+ }
46
+ async receiveFile(requestId, relativePath, totalChunks, fileSize) {
47
+ const absolutePath = join(this.rootPath, relativePath);
48
+ const folderPath = relativePath.includes("/") ? relativePath.substring(0, relativePath.lastIndexOf("/")) : null;
49
+ if (folderPath) this.events.onReceiveStart?.(folderPath);
50
+ const dir = external_node_path_dirname(absolutePath);
51
+ await mkdir(dir, {
52
+ recursive: true
53
+ });
54
+ this.pendingReceives.set(requestId, {
55
+ path: relativePath,
56
+ absolutePath,
57
+ totalChunks,
58
+ fileSize,
59
+ receivedChunks: new Map(),
60
+ receivedSize: 0,
61
+ isSaving: false
62
+ });
63
+ this.events.onReceiveStart?.(relativePath);
64
+ console.log(`[FileTransfer] Ready to receive: ${relativePath} (${fileSize} bytes, ${totalChunks} chunks)`);
65
+ }
66
+ async handleChunk(requestId, index, base64Data) {
67
+ const pending = this.pendingReceives.get(requestId);
68
+ if (!pending) return void console.warn(`[FileTransfer] Unknown requestId: ${requestId}`);
69
+ const chunk = Buffer.from(base64Data, "base64");
70
+ pending.receivedChunks.set(index, chunk);
71
+ pending.receivedSize += chunk.length;
72
+ if (pending.receivedChunks.size === pending.totalChunks && !pending.isSaving) {
73
+ pending.isSaving = true;
74
+ await this.assembleAndSave(requestId, pending);
75
+ }
76
+ }
77
+ async handleTransferComplete(requestId) {
78
+ const pending = this.pendingReceives.get(requestId);
79
+ if (!pending) return;
80
+ if (pending.receivedChunks.size !== pending.totalChunks || pending.isSaving) {
81
+ if (pending.receivedChunks.size < pending.totalChunks) console.warn(`[FileTransfer] Transfer complete but missing chunks: ${pending.receivedChunks.size}/${pending.totalChunks}`);
82
+ } else {
83
+ pending.isSaving = true;
84
+ await this.assembleAndSave(requestId, pending);
85
+ }
86
+ }
87
+ async assembleAndSave(requestId, pending) {
88
+ try {
89
+ const chunks = [];
90
+ for(let i = 0; i < pending.totalChunks; i++){
91
+ const chunk = pending.receivedChunks.get(i);
92
+ if (!chunk) throw new Error(`Missing chunk ${i}`);
93
+ chunks.push(chunk);
94
+ }
95
+ const fileBuffer = Buffer.concat(chunks);
96
+ await writeFile(pending.absolutePath, fileBuffer);
97
+ console.log(`[FileTransfer] File saved: ${pending.path} (${fileBuffer.length} bytes)`);
98
+ this.events.onFileSaved(requestId, pending.path);
99
+ this.pendingReceives.delete(requestId);
100
+ } catch (error) {
101
+ console.error(`[FileTransfer] Failed to save file: ${pending.path}`, error);
102
+ this.events.onError(requestId, ErrorCodes.PERMISSION_DENIED, `Failed to save file: ${pending.path}`);
103
+ this.pendingReceives.delete(requestId);
104
+ }
105
+ }
106
+ async cancelTransfer(requestId) {
107
+ this.activeSends.delete(requestId);
108
+ const pending = this.pendingReceives.get(requestId);
109
+ if (pending) {
110
+ console.log(`[FileTransfer] Transfer cancelled: ${pending.path}`);
111
+ this.pendingReceives.delete(requestId);
112
+ }
113
+ }
114
+ getPendingCount() {
115
+ return this.pendingReceives.size + this.activeSends.size;
116
+ }
117
+ }
118
+ function sleep(ms) {
119
+ return new Promise((resolve)=>setTimeout(resolve, ms));
120
+ }
121
+ const execAsync = promisify(exec);
122
+ class FileChangeCollector {
123
+ pending = new Map();
124
+ timer = null;
125
+ onFlush;
126
+ constructor(onFlush){
127
+ this.onFlush = onFlush;
128
+ }
129
+ add(file) {
130
+ this.pending.set(file.path, file);
131
+ this.scheduleFlush();
132
+ }
133
+ scheduleFlush() {
134
+ if (this.timer) return;
135
+ this.timer = setTimeout(()=>{
136
+ this.flush();
137
+ this.timer = null;
138
+ }, FILE_CHANGE_DEBOUNCE_MS);
139
+ }
140
+ flush() {
141
+ if (0 === this.pending.size) return;
142
+ const files = Array.from(this.pending.values());
143
+ this.pending.clear();
144
+ this.onFlush(files);
145
+ }
146
+ clear() {
147
+ if (this.timer) {
148
+ clearTimeout(this.timer);
149
+ this.timer = null;
150
+ }
151
+ this.pending.clear();
152
+ }
153
+ }
154
+ class FileWatcher {
155
+ rootPath;
156
+ watcher = null;
157
+ events;
158
+ fileCollector;
159
+ processingFiles = new Set();
160
+ constructor(rootPath, events){
161
+ this.rootPath = rootPath;
162
+ this.events = events;
163
+ this.fileCollector = new FileChangeCollector((files)=>{
164
+ if (1 === files.length) this.events.onFileAdded(files[0]);
165
+ else this.events.onFilesAdded(files);
166
+ });
167
+ }
168
+ async scan() {
169
+ const files = [];
170
+ const folders = [];
171
+ await this.scanDirectory(this.rootPath, "", files, folders);
172
+ return {
173
+ files,
174
+ folders
175
+ };
176
+ }
177
+ async scanDirectory(absolutePath, relativePath, files, folders) {
178
+ try {
179
+ const entries = await readdir(absolutePath, {
180
+ withFileTypes: true
181
+ });
182
+ for (const entry of entries){
183
+ const entryPath = join(absolutePath, entry.name);
184
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
185
+ if (entry.isDirectory()) {
186
+ if (entry.name.startsWith(".")) continue;
187
+ const folderInfo = await this.getFolderInfo(entryPath, entryRelativePath);
188
+ folders.push(folderInfo);
189
+ await this.scanDirectory(entryPath, entryRelativePath, files, folders);
190
+ } else if (entry.isFile()) {
191
+ if (isConfigFile(entry.name)) continue;
192
+ if (!isSupportedFile(entry.name)) continue;
193
+ if (entry.name.startsWith(".")) continue;
194
+ const fileInfo = await this.getFileInfo(entryPath, entryRelativePath);
195
+ files.push(fileInfo);
196
+ }
197
+ }
198
+ } catch (error) {
199
+ console.error(`Failed to scan directory ${absolutePath}:`, error);
200
+ }
201
+ }
202
+ async getFileInfo(absolutePath, relativePath) {
203
+ const stat = await promises_stat(absolutePath);
204
+ const name = getFilename(absolutePath);
205
+ const configPath = getFileConfigPath(absolutePath);
206
+ let hasConfig = false;
207
+ let assetId;
208
+ try {
209
+ const configContent = await readFile(configPath, "utf-8");
210
+ const config = parseFileConfig(configContent);
211
+ if (config) {
212
+ hasConfig = true;
213
+ assetId = config.assetId;
214
+ }
215
+ } catch {}
216
+ return {
217
+ path: relativePath,
218
+ name,
219
+ size: stat.size,
220
+ mtime: stat.mtimeMs,
221
+ hasConfig,
222
+ assetId
223
+ };
224
+ }
225
+ async getFolderInfo(absolutePath, relativePath) {
226
+ const name = getFilename(absolutePath);
227
+ const configPath = getFolderConfigPath(absolutePath);
228
+ let hasConfig = false;
229
+ let folderId;
230
+ try {
231
+ const configContent = await readFile(configPath, "utf-8");
232
+ const config = parseFolderConfig(configContent);
233
+ if (config) {
234
+ hasConfig = true;
235
+ folderId = config.folderId;
236
+ }
237
+ } catch {}
238
+ return {
239
+ path: relativePath,
240
+ name,
241
+ hasConfig,
242
+ folderId
243
+ };
244
+ }
245
+ async start() {
246
+ this.watcher = watch(this.rootPath, {
247
+ ignored: [
248
+ /(^|[/\\])\.[^/\\]+$/,
249
+ /node_modules/
250
+ ],
251
+ persistent: true,
252
+ ignoreInitial: true,
253
+ awaitWriteFinish: {
254
+ stabilityThreshold: FILE_STABLE_CHECK_INTERVAL_MS,
255
+ pollInterval: 100
256
+ }
257
+ });
258
+ this.watcher.on("add", (filePath)=>this.handleFileAdd(filePath));
259
+ this.watcher.on("unlink", (filePath)=>this.handleFileRemove(filePath));
260
+ this.watcher.on("addDir", (dirPath)=>this.handleDirAdd(dirPath));
261
+ this.watcher.on("unlinkDir", (dirPath)=>this.handleDirRemove(dirPath));
262
+ this.watcher.on("error", (error)=>console.error("Watcher error:", error));
263
+ }
264
+ async stop() {
265
+ this.fileCollector.clear();
266
+ if (this.watcher) {
267
+ await this.watcher.close();
268
+ this.watcher = null;
269
+ }
270
+ }
271
+ async handleFileAdd(filePath) {
272
+ const relativePath = toRelativePath(filePath, this.rootPath);
273
+ const filename = getFilename(filePath);
274
+ if (isConfigFile(filename)) return;
275
+ if (!isSupportedFile(filename)) return;
276
+ if (filename.startsWith(".")) return;
277
+ if (this.processingFiles.has(relativePath)) return;
278
+ try {
279
+ const fileInfo = await this.getFileInfo(filePath, relativePath);
280
+ if (fileInfo.hasConfig) return void console.log(`[FileWatcher] Skipping already synced file: ${relativePath}`);
281
+ console.log(`[FileWatcher] File added: ${relativePath}`);
282
+ this.fileCollector.add(fileInfo);
283
+ } catch (error) {
284
+ console.error(`[FileWatcher] Failed to get file info: ${filePath}`, error);
285
+ }
286
+ }
287
+ handleFileRemove(filePath) {
288
+ const relativePath = toRelativePath(filePath, this.rootPath);
289
+ const filename = getFilename(filePath);
290
+ if (isConfigFile(filename)) return;
291
+ if (!isSupportedFile(filename)) return;
292
+ console.log(`[FileWatcher] File removed: ${relativePath}`);
293
+ this.events.onFileRemoved(relativePath);
294
+ }
295
+ async handleDirAdd(dirPath) {
296
+ const relativePath = toRelativePath(dirPath, this.rootPath);
297
+ const dirname = getFilename(dirPath);
298
+ if (!relativePath) return;
299
+ if (dirname.startsWith(".")) return;
300
+ if (this.processingFiles.has(relativePath)) return void console.log(`[FileWatcher] Skipping processing folder: ${relativePath}`);
301
+ try {
302
+ const folderInfo = await this.getFolderInfo(dirPath, relativePath);
303
+ if (folderInfo.hasConfig) return void console.log(`[FileWatcher] Skipping already synced folder: ${relativePath}`);
304
+ console.log(`[FileWatcher] Folder added: ${relativePath}`);
305
+ this.events.onFolderAdded(folderInfo);
306
+ } catch (error) {
307
+ console.error(`[FileWatcher] Failed to get folder info: ${dirPath}`, error);
308
+ }
309
+ }
310
+ handleDirRemove(dirPath) {
311
+ const relativePath = toRelativePath(dirPath, this.rootPath);
312
+ if (!relativePath) return;
313
+ console.log(`[FileWatcher] Folder removed: ${relativePath}`);
314
+ this.events.onFolderRemoved(relativePath);
315
+ }
316
+ async writeConfig(relativePath, assetId, folderId, originalFilename, md5) {
317
+ const absolutePath = join(this.rootPath, relativePath);
318
+ if (assetId && originalFilename) {
319
+ const configPath = getFileConfigPath(absolutePath);
320
+ const config = createFileConfig(assetId, originalFilename, md5);
321
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
322
+ await this.setHiddenAttribute(configPath);
323
+ console.log(`[FileWatcher] Config written (hidden): ${configPath}`);
324
+ } else if (folderId) {
325
+ const configPath = getFolderConfigPath(absolutePath);
326
+ const config = createFolderConfig(folderId);
327
+ await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
328
+ await this.setHiddenAttribute(configPath);
329
+ console.log(`[FileWatcher] Folder config written (hidden): ${configPath}`);
330
+ }
331
+ }
332
+ async setHiddenAttribute(filePath) {
333
+ try {
334
+ if ("darwin" === process.platform) await execAsync(`chflags hidden "${filePath}"`);
335
+ else if ("win32" === process.platform) await execAsync(`attrib +h "${filePath}"`);
336
+ } catch (error) {
337
+ console.warn(`[FileWatcher] Failed to set hidden attribute: ${filePath}`, error);
338
+ }
339
+ }
340
+ markProcessing(relativePath) {
341
+ this.processingFiles.add(relativePath);
342
+ }
343
+ unmarkProcessing(relativePath) {
344
+ this.processingFiles.delete(relativePath);
345
+ }
346
+ }
347
+ class SyncServer {
348
+ wss = null;
349
+ client = null;
350
+ fileWatcher = null;
351
+ fileTransfer = null;
352
+ port;
353
+ host;
354
+ cwd;
355
+ syncFolder = null;
356
+ events;
357
+ messageQueue = [];
358
+ isProcessingQueue = false;
359
+ currentProjectId = null;
360
+ constructor(options = {}, events = {}){
361
+ this.port = options.port ?? DEFAULT_PORT;
362
+ this.host = options.host ?? DEFAULT_HOST;
363
+ this.cwd = process.cwd();
364
+ this.events = events;
365
+ }
366
+ async start() {
367
+ return new Promise((resolve, reject)=>{
368
+ try {
369
+ this.wss = new WebSocketServer({
370
+ port: this.port,
371
+ host: this.host
372
+ });
373
+ this.wss.on("listening", ()=>{
374
+ console.log(`🌐 WebSocket server listening on ws://${this.host}:${this.port}`);
375
+ resolve();
376
+ });
377
+ this.wss.on("connection", (ws)=>{
378
+ this.handleConnection(ws);
379
+ });
380
+ this.wss.on("error", (error)=>{
381
+ console.error("WebSocket server error:", error);
382
+ this.events.onError?.(error);
383
+ reject(error);
384
+ });
385
+ } catch (error) {
386
+ reject(error);
387
+ }
388
+ });
389
+ }
390
+ async stop() {
391
+ if (this.fileWatcher) {
392
+ await this.fileWatcher.stop();
393
+ this.fileWatcher = null;
394
+ }
395
+ if (this.client) {
396
+ this.client.close();
397
+ this.client = null;
398
+ }
399
+ return new Promise((resolve)=>{
400
+ if (this.wss) this.wss.close(()=>{
401
+ console.log("🛑 WebSocket server stopped");
402
+ this.wss = null;
403
+ resolve();
404
+ });
405
+ else resolve();
406
+ });
407
+ }
408
+ handleConnection(ws) {
409
+ const pendingClient = ws;
410
+ console.log("📡 New connection attempt, waiting for project registration...");
411
+ pendingClient.send(JSON.stringify(createConnectedMessage(this.cwd)));
412
+ const registrationTimeout = setTimeout(()=>{
413
+ console.log("⚠️ Registration timeout, closing connection");
414
+ pendingClient.close(1008, "Registration timeout");
415
+ }, 5000);
416
+ const tempMessageHandler = (data)=>{
417
+ try {
418
+ const message = JSON.parse(data.toString());
419
+ if ("register_project" === message.type) {
420
+ clearTimeout(registrationTimeout);
421
+ pendingClient.removeListener("message", tempMessageHandler);
422
+ this.handleProjectRegistration(pendingClient, message.projectId);
423
+ }
424
+ } catch (error) {
425
+ console.error("Failed to parse registration message:", error);
426
+ }
427
+ };
428
+ pendingClient.on("message", tempMessageHandler);
429
+ pendingClient.on("close", ()=>{
430
+ clearTimeout(registrationTimeout);
431
+ pendingClient.removeListener("message", tempMessageHandler);
432
+ });
433
+ }
434
+ handleProjectRegistration(ws, projectId) {
435
+ if (this.client && this.currentProjectId === projectId) {
436
+ console.log(`⚠️ Project ${projectId} is already connected, rejecting new connection`);
437
+ ws.send(JSON.stringify(createErrorMessage(ErrorCodes.PROJECT_ALREADY_CONNECTED, `Project ${projectId} is already connected from another client`)));
438
+ ws.close(1013, "Project already connected");
439
+ return;
440
+ }
441
+ if (this.client) {
442
+ console.log(`🔄 Switching from project ${this.currentProjectId} to ${projectId}`);
443
+ this.client.close(1000, "New project connected");
444
+ this.cleanupConnection();
445
+ }
446
+ this.client = ws;
447
+ this.currentProjectId = projectId;
448
+ console.log(`✅ Project ${projectId} registered`);
449
+ this.events.onClientConnected?.(ws);
450
+ ws.on("message", (data)=>{
451
+ try {
452
+ const message = JSON.parse(data.toString());
453
+ this.enqueueMessage(message);
454
+ } catch (error) {
455
+ console.error("Failed to parse message:", error);
456
+ this.send(createErrorMessage(ErrorCodes.UNKNOWN_ERROR, "Failed to parse message"));
457
+ }
458
+ });
459
+ ws.on("close", ()=>{
460
+ console.log("📴 Client disconnected");
461
+ this.cleanupConnection();
462
+ this.events.onClientDisconnected?.(ws);
463
+ });
464
+ ws.on("error", (error)=>{
465
+ console.error("WebSocket error:", error);
466
+ this.events.onError?.(error);
467
+ });
468
+ }
469
+ cleanupConnection() {
470
+ this.client = null;
471
+ this.currentProjectId = null;
472
+ if (this.fileWatcher) {
473
+ this.fileWatcher.stop();
474
+ this.fileWatcher = null;
475
+ }
476
+ this.syncFolder = null;
477
+ }
478
+ async handleMessage(message) {
479
+ switch(message.type){
480
+ case "list_directory":
481
+ await this.handleListDirectory(message.path);
482
+ break;
483
+ case "select_folder":
484
+ await this.handleSelectFolder(message.path);
485
+ break;
486
+ case "request_file":
487
+ await this.handleRequestFile(message.requestId, message.path);
488
+ break;
489
+ case "send_file":
490
+ await this.handleSendFile(message.requestId, message.path, message.totalChunks, message.fileSize);
491
+ break;
492
+ case "file_chunk":
493
+ await this.handleFileChunk(message.requestId, message.index, message.data);
494
+ break;
495
+ case "file_transfer_complete":
496
+ await this.handleFileTransferComplete(message.requestId);
497
+ break;
498
+ case "write_config":
499
+ await this.handleWriteConfig(message.path, message.assetId, message.folderId, message.originalFilename, message.md5);
500
+ break;
501
+ case "cancel_transfer":
502
+ await this.handleCancelTransfer(message.requestId);
503
+ break;
504
+ default:
505
+ console.warn("Unknown message type:", message.type);
506
+ }
507
+ }
508
+ async handleListDirectory(path) {
509
+ const fs = await import("node:fs/promises");
510
+ const nodePath = await import("node:path");
511
+ const targetPath = path ? nodePath.resolve(this.cwd, path) : this.cwd;
512
+ try {
513
+ const entries = await fs.readdir(targetPath, {
514
+ withFileTypes: true
515
+ });
516
+ const directories = entries.filter((entry)=>entry.isDirectory() && !entry.name.startsWith(".")).map((entry)=>entry.name).sort();
517
+ const listing = {
518
+ path: targetPath,
519
+ directories,
520
+ isRoot: targetPath === this.cwd
521
+ };
522
+ this.send({
523
+ type: "directory_listing",
524
+ listing
525
+ });
526
+ } catch (error) {
527
+ this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, `Failed to list directory: ${targetPath}`));
528
+ }
529
+ }
530
+ async handleSelectFolder(path) {
531
+ const fs = await import("node:fs/promises");
532
+ const nodePath = await import("node:path");
533
+ const absolutePath = nodePath.resolve(this.cwd, path);
534
+ try {
535
+ const stat = await fs.stat(absolutePath);
536
+ if (!stat.isDirectory()) return void this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, `Not a directory: ${absolutePath}`));
537
+ } catch {
538
+ this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, `Folder not found: ${absolutePath}`));
539
+ return;
540
+ }
541
+ if (this.fileWatcher) await this.fileWatcher.stop();
542
+ this.syncFolder = absolutePath;
543
+ this.fileWatcher = new FileWatcher(absolutePath, {
544
+ onFileAdded: (file)=>this.send({
545
+ type: "file_added",
546
+ file
547
+ }),
548
+ onFilesAdded: (files)=>this.send({
549
+ type: "files_added",
550
+ files
551
+ }),
552
+ onFileRemoved: (path)=>this.send({
553
+ type: "file_removed",
554
+ path
555
+ }),
556
+ onFolderAdded: (folder)=>this.send({
557
+ type: "folder_added",
558
+ folder
559
+ }),
560
+ onFolderRemoved: (path)=>this.send({
561
+ type: "folder_removed",
562
+ path
563
+ })
564
+ });
565
+ this.fileTransfer = new FileTransferManager(absolutePath, {
566
+ onChunk: (requestId, index, total, data)=>{
567
+ this.send({
568
+ type: "file_chunk",
569
+ requestId,
570
+ index,
571
+ total,
572
+ data
573
+ });
574
+ },
575
+ onComplete: (requestId, path)=>{
576
+ this.send({
577
+ type: "file_transfer_complete",
578
+ requestId,
579
+ path
580
+ });
581
+ },
582
+ onFileSaved: (requestId, path)=>{
583
+ this.send({
584
+ type: "file_saved",
585
+ requestId,
586
+ path
587
+ });
588
+ },
589
+ onError: (requestId, code, message)=>{
590
+ this.send(createErrorMessage(code, message, requestId));
591
+ },
592
+ onReceiveStart: (relativePath)=>{
593
+ this.fileWatcher?.markProcessing(relativePath);
594
+ },
595
+ onReceiveEnd: (relativePath)=>{}
596
+ });
597
+ const snapshot = await this.fileWatcher.scan();
598
+ this.send({
599
+ type: "folder_selected",
600
+ path: absolutePath,
601
+ snapshot
602
+ });
603
+ await this.fileWatcher.start();
604
+ console.log(`📂 Syncing folder: ${absolutePath}`);
605
+ }
606
+ async handleRequestFile(requestId, path) {
607
+ if (!this.fileTransfer) return void this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, "No folder selected", requestId));
608
+ await this.fileTransfer.sendFile(requestId, path);
609
+ }
610
+ async handleSendFile(requestId, path, totalChunks, fileSize) {
611
+ if (!this.fileTransfer) return void this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, "No folder selected", requestId));
612
+ await this.fileTransfer.receiveFile(requestId, path, totalChunks, fileSize);
613
+ }
614
+ async handleFileChunk(requestId, index, data) {
615
+ if (!this.fileTransfer) return void this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, "No folder selected", requestId));
616
+ await this.fileTransfer.handleChunk(requestId, index, data);
617
+ }
618
+ async handleFileTransferComplete(requestId) {
619
+ if (!this.fileTransfer) return;
620
+ await this.fileTransfer.handleTransferComplete(requestId);
621
+ }
622
+ async handleWriteConfig(path, assetId, folderId, originalFilename, md5) {
623
+ if (!this.fileWatcher) return void this.send(createErrorMessage(ErrorCodes.FOLDER_NOT_FOUND, "No folder selected"));
624
+ try {
625
+ await this.fileWatcher.writeConfig(path, assetId, folderId, originalFilename, md5);
626
+ this.fileWatcher.unmarkProcessing(path);
627
+ if (assetId && path.includes("/")) {
628
+ const folderPath = path.substring(0, path.lastIndexOf("/"));
629
+ this.fileWatcher.unmarkProcessing(folderPath);
630
+ }
631
+ this.send({
632
+ type: "config_written",
633
+ path,
634
+ assetId,
635
+ folderId
636
+ });
637
+ } catch (error) {
638
+ this.fileWatcher.unmarkProcessing(path);
639
+ if (assetId && path.includes("/")) {
640
+ const folderPath = path.substring(0, path.lastIndexOf("/"));
641
+ this.fileWatcher.unmarkProcessing(folderPath);
642
+ }
643
+ this.send(createErrorMessage(ErrorCodes.PERMISSION_DENIED, `Failed to write config: ${error}`));
644
+ }
645
+ }
646
+ async handleCancelTransfer(requestId) {
647
+ if (!this.fileTransfer) return;
648
+ await this.fileTransfer.cancelTransfer(requestId);
649
+ }
650
+ send(message) {
651
+ if (this.client && 1 === this.client.readyState) this.client.send(JSON.stringify(message));
652
+ }
653
+ getAddress() {
654
+ return `ws://${this.host}:${this.port}`;
655
+ }
656
+ isClientConnected() {
657
+ return null !== this.client && 1 === this.client.readyState;
658
+ }
659
+ enqueueMessage(message) {
660
+ this.messageQueue.push(message);
661
+ this.processQueue();
662
+ }
663
+ async processQueue() {
664
+ if (this.isProcessingQueue) return;
665
+ this.isProcessingQueue = true;
666
+ while(this.messageQueue.length > 0){
667
+ const message = this.messageQueue.shift();
668
+ try {
669
+ await this.handleMessage(message);
670
+ } catch (error) {
671
+ console.error("Error handling message:", error);
672
+ }
673
+ }
674
+ this.isProcessingQueue = false;
675
+ }
12
676
  }
13
677
  const program = new Command();
14
- program.name("syncast-cli").description("Syncast CLI tool").version("0.1.0");
15
- program.command("start").description("Start the Syncast server").option("-p, --port <port>", "Port to listen on", "3456").option("-h, --host <host>", "Host to bind to", "localhost").action(async (options)=>{
678
+ program.name("syncast-cli").description("Syncast CLI tool for local file sync").version("0.1.0");
679
+ program.command("sync").description("Start the local sync server").option("-p, --port <port>", "Port to listen on", String(DEFAULT_PORT)).option("-H, --host <host>", "Host to bind to", DEFAULT_HOST).action(startSyncServer);
680
+ async function startSyncServer(options) {
16
681
  const port = Number.parseInt(options.port, 10);
17
682
  const host = options.host;
18
- console.log("🔧 Syncast CLI");
683
+ console.log("");
684
+ console.log("🔧 Syncast Local Sync");
19
685
  console.log(" Version: 0.1.0");
20
686
  console.log(` Port: ${port}`);
21
687
  console.log(` Host: ${host}`);
688
+ console.log(` Working Directory: ${process.cwd()}`);
22
689
  console.log("");
23
- const server = await createServer({
690
+ const server = new SyncServer({
24
691
  port,
25
692
  host
693
+ }, {
694
+ onClientConnected: ()=>{
695
+ console.log("📱 Frontend connected");
696
+ },
697
+ onClientDisconnected: ()=>{
698
+ console.log("📴 Frontend disconnected");
699
+ },
700
+ onError: (error)=>{
701
+ console.error("❌ Server error:", error.message);
702
+ }
26
703
  });
27
- await server.start();
28
- process.on("SIGINT", async ()=>{
704
+ try {
705
+ await server.start();
706
+ console.log("");
707
+ console.log("✨ Ready to sync! Connect from Syncast frontend.");
708
+ console.log(` Address: ${server.getAddress()}`);
709
+ console.log("");
710
+ console.log("Press Ctrl+C to stop.");
711
+ console.log("");
712
+ } catch (error) {
713
+ console.error("Failed to start server:", error);
714
+ process.exit(1);
715
+ }
716
+ const shutdown = async ()=>{
29
717
  console.log("\n");
718
+ console.log("🛑 Shutting down...");
30
719
  await server.stop();
31
720
  process.exit(0);
32
- });
33
- process.on("SIGTERM", async ()=>{
34
- await server.stop();
35
- process.exit(0);
36
- });
721
+ };
722
+ process.on("SIGINT", shutdown);
723
+ process.on("SIGTERM", shutdown);
37
724
  await new Promise(()=>{});
38
- });
39
- program.command("status").description("Check the status of Syncast server").action(()=>{
40
- console.log("📊 Checking Syncast server status...");
725
+ }
726
+ program.command("start").description("Alias for 'sync' command - Start the local sync server").option("-p, --port <port>", "Port to listen on", String(DEFAULT_PORT)).option("-H, --host <host>", "Host to bind to", DEFAULT_HOST).action(startSyncServer);
727
+ program.command("status").description("Check if a sync server is running").option("-p, --port <port>", "Port to check", String(DEFAULT_PORT)).option("-H, --host <host>", "Host to check", DEFAULT_HOST).action(async (options)=>{
728
+ const port = Number.parseInt(options.port, 10);
729
+ const host = options.host;
730
+ const address = `ws://${host}:${port}`;
731
+ console.log(`📊 Checking sync server at ${address}...`);
732
+ try {
733
+ const { WebSocket } = await import("ws");
734
+ const ws = new WebSocket(address);
735
+ const timeout = setTimeout(()=>{
736
+ ws.close();
737
+ console.log("❌ No server running (timeout)");
738
+ process.exit(1);
739
+ }, 3000);
740
+ ws.on("open", ()=>{
741
+ clearTimeout(timeout);
742
+ console.log("✅ Server is running");
743
+ ws.close();
744
+ process.exit(0);
745
+ });
746
+ ws.on("error", ()=>{
747
+ clearTimeout(timeout);
748
+ console.log("❌ No server running");
749
+ process.exit(1);
750
+ });
751
+ } catch {
752
+ console.log("❌ Failed to check server status");
753
+ process.exit(1);
754
+ }
41
755
  });
42
756
  async function runCli(args = process.argv) {
43
757
  await program.parseAsync(args);
44
758
  }
45
759
  runCli();
46
- async function src_createServer(options = {}) {
47
- const { port = 3456, host = "localhost" } = options;
48
- return {
49
- async start () {
50
- console.log(`🚀 Syncast server starting on ${host}:${port}`);
51
- },
52
- async stop () {
53
- console.log("🛑 Syncast server stopped");
54
- }
55
- };
56
- }
57
- export { src_createServer as createServer, runCli };
760
+ export { DEFAULT_HOST, DEFAULT_PORT, FileTransferManager, FileWatcher, PROTOCOL_VERSION, SyncServer, runCli };