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/cli.js +865 -58
- package/dist/file-transfer.d.ts +52 -0
- package/dist/file-watcher.d.ts +80 -0
- package/dist/index.d.ts +4 -9
- package/dist/index.js +863 -33
- package/dist/ws-server.d.ts +106 -0
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -1,80 +1,887 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
import { mkdir, readFile, readdir, stat as promises_stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname as external_node_path_dirname, join } from "node:path";
|
|
6
|
+
import { exec } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { watch } from "chokidar";
|
|
9
|
+
const DEFAULT_HOST = "localhost";
|
|
10
|
+
const CHUNK_SIZE = 1048576;
|
|
11
|
+
const FILE_CONFIG_SUFFIX = ".config.json";
|
|
12
|
+
const FOLDER_CONFIG_SUFFIX = ".folder.config.json";
|
|
13
|
+
const SUPPORTED_EXTENSIONS = [
|
|
14
|
+
"jpg",
|
|
15
|
+
"jpeg",
|
|
16
|
+
"png",
|
|
17
|
+
"webp",
|
|
18
|
+
"gif",
|
|
19
|
+
"mp4",
|
|
20
|
+
"mov",
|
|
21
|
+
"m4v",
|
|
22
|
+
"webm",
|
|
23
|
+
"mp3",
|
|
24
|
+
"wav",
|
|
25
|
+
"ogg",
|
|
26
|
+
"m4a"
|
|
27
|
+
];
|
|
28
|
+
function createFileConfig(assetId, originalFilename, md5) {
|
|
29
|
+
return {
|
|
30
|
+
assetId,
|
|
31
|
+
syncedAt: new Date().toISOString(),
|
|
32
|
+
originalFilename,
|
|
33
|
+
md5
|
|
34
|
+
};
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
async function createServer(options = {}) {
|
|
37
|
-
const { port = 3456, host = "localhost" } = options;
|
|
36
|
+
function createFolderConfig(folderId) {
|
|
38
37
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
folderId,
|
|
39
|
+
syncedAt: new Date().toISOString()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function parseFileConfig(content) {
|
|
43
|
+
try {
|
|
44
|
+
const config = JSON.parse(content);
|
|
45
|
+
if (!config.assetId || "string" != typeof config.assetId) {
|
|
46
|
+
console.warn("Invalid config.json: missing or invalid assetId");
|
|
47
|
+
return null;
|
|
44
48
|
}
|
|
49
|
+
if (!config.originalFilename || "string" != typeof config.originalFilename) {
|
|
50
|
+
console.warn("Invalid config.json: missing or invalid originalFilename");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return config;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn("Failed to parse config.json:", e);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function parseFolderConfig(content) {
|
|
60
|
+
try {
|
|
61
|
+
const config = JSON.parse(content);
|
|
62
|
+
if (!config.folderId || "string" != typeof config.folderId) {
|
|
63
|
+
console.warn("Invalid folder config.json: missing or invalid folderId");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return config;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.warn("Failed to parse folder config.json:", e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getFileConfigPath(filePath) {
|
|
73
|
+
return `${filePath}${FILE_CONFIG_SUFFIX}`;
|
|
74
|
+
}
|
|
75
|
+
function getFolderConfigPath(folderPath) {
|
|
76
|
+
const normalizedPath = folderPath.replace(/\/+$/, "");
|
|
77
|
+
return `${normalizedPath}${FOLDER_CONFIG_SUFFIX}`;
|
|
78
|
+
}
|
|
79
|
+
function isConfigFile(path) {
|
|
80
|
+
return path.endsWith(FOLDER_CONFIG_SUFFIX) || path.endsWith(FILE_CONFIG_SUFFIX);
|
|
81
|
+
}
|
|
82
|
+
function getFileExtension(filename) {
|
|
83
|
+
const lastDot = filename.lastIndexOf(".");
|
|
84
|
+
if (-1 === lastDot || lastDot === filename.length - 1) return "";
|
|
85
|
+
return filename.slice(lastDot + 1).toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
function isSupportedFile(filename) {
|
|
88
|
+
const ext = getFileExtension(filename);
|
|
89
|
+
return ext ? SUPPORTED_EXTENSIONS.includes(ext) : false;
|
|
90
|
+
}
|
|
91
|
+
function getFilename(path) {
|
|
92
|
+
const normalized = path.replace(/\\/g, "/");
|
|
93
|
+
const parts = normalized.split("/");
|
|
94
|
+
return parts[parts.length - 1] || "";
|
|
95
|
+
}
|
|
96
|
+
function normalizePath(path) {
|
|
97
|
+
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
98
|
+
}
|
|
99
|
+
function toRelativePath(absolutePath, basePath) {
|
|
100
|
+
const normalizedAbs = normalizePath(absolutePath);
|
|
101
|
+
const normalizedBase = normalizePath(basePath);
|
|
102
|
+
if (!normalizedAbs.startsWith(normalizedBase)) return normalizedAbs;
|
|
103
|
+
let relative = normalizedAbs.slice(normalizedBase.length);
|
|
104
|
+
if (relative.startsWith("/")) relative = relative.slice(1);
|
|
105
|
+
return relative;
|
|
106
|
+
}
|
|
107
|
+
function createConnectedMessage(cwd) {
|
|
108
|
+
return {
|
|
109
|
+
type: "connected",
|
|
110
|
+
cwd,
|
|
111
|
+
version: "1.0.0"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function createErrorMessage(code, message, requestId) {
|
|
115
|
+
return {
|
|
116
|
+
type: "error",
|
|
117
|
+
code,
|
|
118
|
+
message,
|
|
119
|
+
requestId
|
|
45
120
|
};
|
|
46
121
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
122
|
+
const ErrorCodes = {
|
|
123
|
+
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
124
|
+
FOLDER_NOT_FOUND: "FOLDER_NOT_FOUND",
|
|
125
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
126
|
+
FILE_IN_USE: "FILE_IN_USE",
|
|
127
|
+
TRANSFER_CANCELLED: "TRANSFER_CANCELLED",
|
|
128
|
+
INVALID_REQUEST_ID: "INVALID_REQUEST_ID",
|
|
129
|
+
VERSION_MISMATCH: "VERSION_MISMATCH",
|
|
130
|
+
PROJECT_ALREADY_CONNECTED: "PROJECT_ALREADY_CONNECTED",
|
|
131
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
132
|
+
};
|
|
133
|
+
var __webpack_exports__DEFAULT_PORT = 23456;
|
|
134
|
+
var __webpack_exports__FILE_CHANGE_DEBOUNCE_MS = 300;
|
|
135
|
+
var __webpack_exports__FILE_STABLE_CHECK_INTERVAL_MS = 500;
|
|
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
|
+
}
|
|
804
|
+
const program = new Command();
|
|
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) {
|
|
50
808
|
const port = Number.parseInt(options.port, 10);
|
|
51
809
|
const host = options.host;
|
|
52
|
-
console.log("
|
|
810
|
+
console.log("");
|
|
811
|
+
console.log("🔧 Syncast Local Sync");
|
|
53
812
|
console.log(" Version: 0.1.0");
|
|
54
813
|
console.log(` Port: ${port}`);
|
|
55
814
|
console.log(` Host: ${host}`);
|
|
815
|
+
console.log(` Working Directory: ${process.cwd()}`);
|
|
56
816
|
console.log("");
|
|
57
|
-
const server =
|
|
817
|
+
const server = new SyncServer({
|
|
58
818
|
port,
|
|
59
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
|
+
}
|
|
60
830
|
});
|
|
61
|
-
|
|
62
|
-
|
|
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 ()=>{
|
|
63
844
|
console.log("\n");
|
|
845
|
+
console.log("🛑 Shutting down...");
|
|
64
846
|
await server.stop();
|
|
65
847
|
process.exit(0);
|
|
66
|
-
}
|
|
67
|
-
process.on("
|
|
68
|
-
|
|
69
|
-
process.exit(0);
|
|
70
|
-
});
|
|
848
|
+
};
|
|
849
|
+
process.on("SIGINT", shutdown);
|
|
850
|
+
process.on("SIGTERM", shutdown);
|
|
71
851
|
await new Promise(()=>{});
|
|
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
|
+
}
|
|
72
882
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
async function cli_runCli(args = process.argv) {
|
|
77
|
-
await cli_program.parseAsync(args);
|
|
883
|
+
async function runCli(args = process.argv) {
|
|
884
|
+
await program.parseAsync(args);
|
|
78
885
|
}
|
|
79
|
-
|
|
80
|
-
export {
|
|
886
|
+
runCli();
|
|
887
|
+
export { runCli };
|