syncast-cli 0.1.0 → 0.1.2

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