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