@involvex/syncstuff-cli 0.0.5 → 0.0.7
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 +387 -1
- package/package.json +1 -1
- package/src/cli/commands/help.ts +38 -0
- package/src/cli/commands/listen.ts +142 -0
- package/src/cli/commands/scan.ts +156 -0
- package/src/cli/index.ts +12 -0
- package/src/utils/network.ts +207 -0
package/dist/cli.js
CHANGED
|
@@ -9691,6 +9691,7 @@ async function showHelp(command) {
|
|
|
9691
9691
|
`) + " " + source_default.green("devices") + ` List all your connected devices
|
|
9692
9692
|
` + " " + source_default.green("device --list") + ` List available devices
|
|
9693
9693
|
` + " " + source_default.green("device <id>") + ` Connect to a specific device
|
|
9694
|
+
` + " " + source_default.green("scan") + ` Scan local network for devices
|
|
9694
9695
|
` + " " + source_default.green("transfer <file>") + ` Transfer a file to a device
|
|
9695
9696
|
|
|
9696
9697
|
` + source_default.bold(`General:
|
|
@@ -9787,7 +9788,40 @@ Show CLI version.
|
|
|
9787
9788
|
${source_default.bold("Usage:")}
|
|
9788
9789
|
syncstuff version
|
|
9789
9790
|
syncstuff --version
|
|
9790
|
-
syncstuff -v
|
|
9791
|
+
syncstuff -v`,
|
|
9792
|
+
scan: `${source_default.cyan.bold("scan")}
|
|
9793
|
+
|
|
9794
|
+
Scan local network for SyncStuff devices.
|
|
9795
|
+
|
|
9796
|
+
${source_default.bold("Usage:")}
|
|
9797
|
+
syncstuff scan [options]
|
|
9798
|
+
|
|
9799
|
+
${source_default.bold("Options:")}
|
|
9800
|
+
--timeout N Set scan timeout in seconds (default: 10)
|
|
9801
|
+
--watch, -w Continuously scan and show new devices
|
|
9802
|
+
|
|
9803
|
+
${source_default.bold("Description:")}
|
|
9804
|
+
Discovers SyncStuff devices on your local network using UDP broadcast.
|
|
9805
|
+
This works without requiring cloud authentication.
|
|
9806
|
+
|
|
9807
|
+
${source_default.bold("Examples:")}
|
|
9808
|
+
syncstuff scan Scan for 10 seconds
|
|
9809
|
+
syncstuff scan --timeout 30 Scan for 30 seconds
|
|
9810
|
+
syncstuff scan --watch Continuously monitor for devices`,
|
|
9811
|
+
listen: `${source_default.cyan.bold("listen")}
|
|
9812
|
+
|
|
9813
|
+
Start listening for incoming file transfers.
|
|
9814
|
+
|
|
9815
|
+
${source_default.bold("Usage:")}
|
|
9816
|
+
syncstuff listen
|
|
9817
|
+
|
|
9818
|
+
${source_default.bold("Description:")}
|
|
9819
|
+
Starts a local HTTP server and advertises this CLI on the network.
|
|
9820
|
+
Allows other SyncStuff devices (like the Android app) to discover
|
|
9821
|
+
this computer and send files to it directly via local network.
|
|
9822
|
+
|
|
9823
|
+
${source_default.bold("Examples:")}
|
|
9824
|
+
syncstuff listen`
|
|
9791
9825
|
};
|
|
9792
9826
|
});
|
|
9793
9827
|
|
|
@@ -31496,6 +31530,346 @@ var init_transfer = __esm(() => {
|
|
|
31496
31530
|
init_ui();
|
|
31497
31531
|
});
|
|
31498
31532
|
|
|
31533
|
+
// src/utils/network.ts
|
|
31534
|
+
import { createSocket } from "dgram";
|
|
31535
|
+
import { networkInterfaces } from "os";
|
|
31536
|
+
|
|
31537
|
+
class NetworkScanner {
|
|
31538
|
+
socket = null;
|
|
31539
|
+
getLocalIPs() {
|
|
31540
|
+
const interfaces = networkInterfaces();
|
|
31541
|
+
const ips = [];
|
|
31542
|
+
for (const name of Object.keys(interfaces)) {
|
|
31543
|
+
const iface = interfaces[name];
|
|
31544
|
+
if (!iface)
|
|
31545
|
+
continue;
|
|
31546
|
+
for (const addr of iface) {
|
|
31547
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
31548
|
+
ips.push(addr.address);
|
|
31549
|
+
}
|
|
31550
|
+
}
|
|
31551
|
+
}
|
|
31552
|
+
return ips;
|
|
31553
|
+
}
|
|
31554
|
+
async scan(timeout = 1e4) {
|
|
31555
|
+
return new Promise((resolve2) => {
|
|
31556
|
+
const devices = [];
|
|
31557
|
+
const seenIds = new Set;
|
|
31558
|
+
try {
|
|
31559
|
+
this.socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
31560
|
+
this.socket.on("error", (err) => {
|
|
31561
|
+
console.error("Scanner error:", err.message);
|
|
31562
|
+
this.cleanup();
|
|
31563
|
+
resolve2(devices);
|
|
31564
|
+
});
|
|
31565
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
31566
|
+
try {
|
|
31567
|
+
const data = JSON.parse(msg.toString());
|
|
31568
|
+
if (data.service === "syncstuff" && data.deviceId) {
|
|
31569
|
+
if (!seenIds.has(data.deviceId)) {
|
|
31570
|
+
seenIds.add(data.deviceId);
|
|
31571
|
+
devices.push({
|
|
31572
|
+
id: data.deviceId,
|
|
31573
|
+
name: data.deviceName || "Unknown Device",
|
|
31574
|
+
platform: data.platform || "unknown",
|
|
31575
|
+
ip: rinfo.address,
|
|
31576
|
+
port: data.port || 8080,
|
|
31577
|
+
version: data.version || "1.0.0"
|
|
31578
|
+
});
|
|
31579
|
+
}
|
|
31580
|
+
}
|
|
31581
|
+
} catch {}
|
|
31582
|
+
});
|
|
31583
|
+
this.socket.bind(0, () => {
|
|
31584
|
+
this.socket?.setBroadcast(true);
|
|
31585
|
+
const discoveryMessage = JSON.stringify({
|
|
31586
|
+
service: "syncstuff",
|
|
31587
|
+
action: "discover",
|
|
31588
|
+
timestamp: Date.now()
|
|
31589
|
+
});
|
|
31590
|
+
const localIPs = this.getLocalIPs();
|
|
31591
|
+
for (const ip of localIPs) {
|
|
31592
|
+
const parts = ip.split(".");
|
|
31593
|
+
parts[3] = "255";
|
|
31594
|
+
const broadcastAddr = parts.join(".");
|
|
31595
|
+
this.socket?.send(discoveryMessage, 0, discoveryMessage.length, SYNCSTUFF_PORT, broadcastAddr);
|
|
31596
|
+
}
|
|
31597
|
+
this.socket?.send(discoveryMessage, 0, discoveryMessage.length, SYNCSTUFF_PORT, "224.0.0.251");
|
|
31598
|
+
});
|
|
31599
|
+
setTimeout(() => {
|
|
31600
|
+
this.cleanup();
|
|
31601
|
+
resolve2(devices);
|
|
31602
|
+
}, timeout);
|
|
31603
|
+
} catch (error3) {
|
|
31604
|
+
console.error("Failed to start scanner:", error3);
|
|
31605
|
+
resolve2(devices);
|
|
31606
|
+
}
|
|
31607
|
+
});
|
|
31608
|
+
}
|
|
31609
|
+
cleanup() {
|
|
31610
|
+
if (this.socket) {
|
|
31611
|
+
try {
|
|
31612
|
+
this.socket.close();
|
|
31613
|
+
} catch {}
|
|
31614
|
+
this.socket = null;
|
|
31615
|
+
}
|
|
31616
|
+
}
|
|
31617
|
+
async sendTo(ip, port, message) {
|
|
31618
|
+
return new Promise((resolve2, reject) => {
|
|
31619
|
+
const socket = createSocket("udp4");
|
|
31620
|
+
const data = JSON.stringify(message);
|
|
31621
|
+
socket.send(data, 0, data.length, port, ip, (err) => {
|
|
31622
|
+
socket.close();
|
|
31623
|
+
if (err) {
|
|
31624
|
+
reject(err);
|
|
31625
|
+
} else {
|
|
31626
|
+
resolve2();
|
|
31627
|
+
}
|
|
31628
|
+
});
|
|
31629
|
+
});
|
|
31630
|
+
}
|
|
31631
|
+
startAdvertising(deviceName, port, platform = "cli") {
|
|
31632
|
+
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
31633
|
+
socket.bind(() => {
|
|
31634
|
+
socket.setBroadcast(true);
|
|
31635
|
+
});
|
|
31636
|
+
const advertise = () => {
|
|
31637
|
+
const message = JSON.stringify({
|
|
31638
|
+
service: "syncstuff",
|
|
31639
|
+
deviceId: "cli-device-" + Math.floor(Math.random() * 1e4),
|
|
31640
|
+
deviceName,
|
|
31641
|
+
platform,
|
|
31642
|
+
port,
|
|
31643
|
+
version: "0.0.6",
|
|
31644
|
+
timestamp: Date.now()
|
|
31645
|
+
});
|
|
31646
|
+
const localIPs = this.getLocalIPs();
|
|
31647
|
+
for (const ip of localIPs) {
|
|
31648
|
+
const parts = ip.split(".");
|
|
31649
|
+
parts[3] = "255";
|
|
31650
|
+
const broadcastAddr = parts.join(".");
|
|
31651
|
+
socket.send(message, 0, message.length, SYNCSTUFF_PORT, broadcastAddr);
|
|
31652
|
+
}
|
|
31653
|
+
socket.send(message, 0, message.length, SYNCSTUFF_PORT, "224.0.0.251");
|
|
31654
|
+
};
|
|
31655
|
+
advertise();
|
|
31656
|
+
return setInterval(advertise, 3000);
|
|
31657
|
+
}
|
|
31658
|
+
}
|
|
31659
|
+
var SYNCSTUFF_PORT = 5353, networkScanner;
|
|
31660
|
+
var init_network = __esm(() => {
|
|
31661
|
+
networkScanner = new NetworkScanner;
|
|
31662
|
+
});
|
|
31663
|
+
|
|
31664
|
+
// src/cli/commands/scan.ts
|
|
31665
|
+
var exports_scan = {};
|
|
31666
|
+
__export(exports_scan, {
|
|
31667
|
+
scanLocal: () => scanLocal
|
|
31668
|
+
});
|
|
31669
|
+
async function scanLocal(args, ctx) {
|
|
31670
|
+
const timeout = args.includes("--timeout") ? parseInt(args[args.indexOf("--timeout") + 1] || "10", 10) : 10;
|
|
31671
|
+
const continuous = args.includes("--watch") || args.includes("-w");
|
|
31672
|
+
printHeader();
|
|
31673
|
+
debugLog(ctx, "Scan command", { timeout, continuous });
|
|
31674
|
+
console.log(source_default.cyan("Scanning local network for SyncStuff devices..."));
|
|
31675
|
+
console.log(source_default.gray(`Timeout: ${timeout} seconds. Use --timeout N to change.
|
|
31676
|
+
`));
|
|
31677
|
+
if (continuous) {
|
|
31678
|
+
await continuousScan(ctx, timeout);
|
|
31679
|
+
} else {
|
|
31680
|
+
await singleScan(ctx, timeout);
|
|
31681
|
+
}
|
|
31682
|
+
}
|
|
31683
|
+
async function singleScan(ctx, timeout) {
|
|
31684
|
+
const spinner = createSpinner("Scanning network...");
|
|
31685
|
+
spinner.start();
|
|
31686
|
+
try {
|
|
31687
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
31688
|
+
if (devices.length === 0) {
|
|
31689
|
+
spinner.info("No devices found on local network");
|
|
31690
|
+
printSeparator();
|
|
31691
|
+
info2("Make sure SyncStuff is running on other devices on this network.");
|
|
31692
|
+
info2("Devices must be on the same local network (Wi-Fi or Ethernet).");
|
|
31693
|
+
printSeparator();
|
|
31694
|
+
return;
|
|
31695
|
+
}
|
|
31696
|
+
spinner.succeed(`Found ${devices.length} device(s) on local network`);
|
|
31697
|
+
printSeparator();
|
|
31698
|
+
displayDevices(devices);
|
|
31699
|
+
printSeparator();
|
|
31700
|
+
success2(`Scan complete. Found ${devices.length} device(s).`);
|
|
31701
|
+
info2("Use 'syncstuff transfer <file>' to send files to these devices.");
|
|
31702
|
+
printSeparator();
|
|
31703
|
+
} catch (err) {
|
|
31704
|
+
spinner.fail("Network scan failed");
|
|
31705
|
+
error2(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31706
|
+
debugLog(ctx, "Scan error", { error: err });
|
|
31707
|
+
}
|
|
31708
|
+
}
|
|
31709
|
+
async function continuousScan(ctx, timeout) {
|
|
31710
|
+
console.log(source_default.cyan(`Watch mode enabled. Press Ctrl+C to exit.
|
|
31711
|
+
`));
|
|
31712
|
+
process.on("SIGINT", () => {
|
|
31713
|
+
console.log(`
|
|
31714
|
+
`);
|
|
31715
|
+
info2("Scan stopped by user");
|
|
31716
|
+
process.exit(0);
|
|
31717
|
+
});
|
|
31718
|
+
const knownDevices = new Map;
|
|
31719
|
+
while (true) {
|
|
31720
|
+
const spinner = createSpinner("Scanning...");
|
|
31721
|
+
spinner.start();
|
|
31722
|
+
try {
|
|
31723
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
31724
|
+
for (const device2 of devices) {
|
|
31725
|
+
if (!knownDevices.has(device2.id)) {
|
|
31726
|
+
spinner.succeed(`New device found: ${source_default.cyan(device2.name)}`);
|
|
31727
|
+
knownDevices.set(device2.id, device2);
|
|
31728
|
+
}
|
|
31729
|
+
}
|
|
31730
|
+
for (const [id, device2] of knownDevices.entries()) {
|
|
31731
|
+
if (!devices.find((d) => d.id === id)) {
|
|
31732
|
+
warning2(`Device lost: ${device2.name}`);
|
|
31733
|
+
knownDevices.delete(id);
|
|
31734
|
+
}
|
|
31735
|
+
}
|
|
31736
|
+
if (devices.length > 0) {
|
|
31737
|
+
spinner.stop();
|
|
31738
|
+
console.clear();
|
|
31739
|
+
printHeader();
|
|
31740
|
+
console.log(source_default.cyan(`Watch mode - ${new Date().toLocaleTimeString()} - Press Ctrl+C to exit.
|
|
31741
|
+
`));
|
|
31742
|
+
displayDevices(devices);
|
|
31743
|
+
} else {
|
|
31744
|
+
spinner.info("No devices found. Retrying...");
|
|
31745
|
+
}
|
|
31746
|
+
} catch (err) {
|
|
31747
|
+
spinner.fail("Scan error, retrying...");
|
|
31748
|
+
debugLog(ctx, "Watch scan error", { error: err });
|
|
31749
|
+
}
|
|
31750
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3000));
|
|
31751
|
+
}
|
|
31752
|
+
}
|
|
31753
|
+
function displayDevices(devices) {
|
|
31754
|
+
const tableData = devices.map((device2) => [
|
|
31755
|
+
device2.id.substring(0, 8) + "...",
|
|
31756
|
+
device2.name,
|
|
31757
|
+
device2.platform,
|
|
31758
|
+
`${device2.ip}:${device2.port}`,
|
|
31759
|
+
device2.version,
|
|
31760
|
+
source_default.green("Available")
|
|
31761
|
+
]);
|
|
31762
|
+
const table = createTable(tableData, [
|
|
31763
|
+
"ID",
|
|
31764
|
+
"Name",
|
|
31765
|
+
"Platform",
|
|
31766
|
+
"Address",
|
|
31767
|
+
"Version",
|
|
31768
|
+
"Status"
|
|
31769
|
+
]);
|
|
31770
|
+
console.log(table);
|
|
31771
|
+
}
|
|
31772
|
+
var init_scan = __esm(() => {
|
|
31773
|
+
init_source();
|
|
31774
|
+
init_network();
|
|
31775
|
+
init_ui();
|
|
31776
|
+
});
|
|
31777
|
+
|
|
31778
|
+
// src/cli/commands/listen.ts
|
|
31779
|
+
var exports_listen = {};
|
|
31780
|
+
__export(exports_listen, {
|
|
31781
|
+
listen: () => listen
|
|
31782
|
+
});
|
|
31783
|
+
import { createServer } from "http";
|
|
31784
|
+
import { networkInterfaces as networkInterfaces2 } from "os";
|
|
31785
|
+
async function listen(_args, ctx) {
|
|
31786
|
+
printHeader();
|
|
31787
|
+
debugLog(ctx, "Starting listen command");
|
|
31788
|
+
console.log(source_default.cyan("Starting SyncStuff CLI Listener..."));
|
|
31789
|
+
console.log(source_default.gray(`This allows other devices to discover this CLI and send files directly.
|
|
31790
|
+
`));
|
|
31791
|
+
const spinner = createSpinner("Initializing network listener...");
|
|
31792
|
+
spinner.start();
|
|
31793
|
+
try {
|
|
31794
|
+
const server = createServer(handleRequest);
|
|
31795
|
+
server.listen(PORT, "0.0.0.0", () => {
|
|
31796
|
+
spinner.succeed(`HTTP Server running on port ${PORT}`);
|
|
31797
|
+
const ips = getLocalIPs();
|
|
31798
|
+
if (ips.length > 0) {
|
|
31799
|
+
info2("Listening on:");
|
|
31800
|
+
ips.forEach((ip) => {
|
|
31801
|
+
console.log(` http://${ip}:${PORT}`);
|
|
31802
|
+
});
|
|
31803
|
+
}
|
|
31804
|
+
startAdvertising();
|
|
31805
|
+
});
|
|
31806
|
+
server.on("error", (err) => {
|
|
31807
|
+
spinner.fail("Failed to start server");
|
|
31808
|
+
error2(`Server error: ${err.message}`);
|
|
31809
|
+
process.exit(1);
|
|
31810
|
+
});
|
|
31811
|
+
} catch (err) {
|
|
31812
|
+
spinner.fail("Failed to start listener");
|
|
31813
|
+
error2(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31814
|
+
process.exit(1);
|
|
31815
|
+
}
|
|
31816
|
+
}
|
|
31817
|
+
function handleRequest(req, res) {
|
|
31818
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
31819
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
31820
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
31821
|
+
if (req.method === "OPTIONS") {
|
|
31822
|
+
res.writeHead(200);
|
|
31823
|
+
res.end();
|
|
31824
|
+
return;
|
|
31825
|
+
}
|
|
31826
|
+
if (req.method === "GET" && req.url === "/status") {
|
|
31827
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
31828
|
+
res.end(JSON.stringify({ status: "online", type: "cli", version: "0.0.6" }));
|
|
31829
|
+
return;
|
|
31830
|
+
}
|
|
31831
|
+
if (req.method === "POST" && req.url === "/upload") {
|
|
31832
|
+
info2(`
|
|
31833
|
+
Incoming transfer request from ${req.socket.remoteAddress}`);
|
|
31834
|
+
let dataLength = 0;
|
|
31835
|
+
req.on("data", (chunk) => {
|
|
31836
|
+
dataLength += chunk.length;
|
|
31837
|
+
});
|
|
31838
|
+
req.on("end", () => {
|
|
31839
|
+
success2(`Received ${dataLength} bytes`);
|
|
31840
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
31841
|
+
res.end(JSON.stringify({ success: true, message: "File received" }));
|
|
31842
|
+
});
|
|
31843
|
+
return;
|
|
31844
|
+
}
|
|
31845
|
+
res.writeHead(404);
|
|
31846
|
+
res.end("Not Found");
|
|
31847
|
+
}
|
|
31848
|
+
function getLocalIPs() {
|
|
31849
|
+
const nets = networkInterfaces2();
|
|
31850
|
+
const results = [];
|
|
31851
|
+
for (const name of Object.keys(nets)) {
|
|
31852
|
+
for (const net of nets[name] || []) {
|
|
31853
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
31854
|
+
results.push(net.address);
|
|
31855
|
+
}
|
|
31856
|
+
}
|
|
31857
|
+
}
|
|
31858
|
+
return results;
|
|
31859
|
+
}
|
|
31860
|
+
function startAdvertising() {
|
|
31861
|
+
info2(`
|
|
31862
|
+
Advertising CLI on local network...`);
|
|
31863
|
+
networkScanner.startAdvertising("SyncStuff CLI", PORT);
|
|
31864
|
+
info2("Ready to receive files. Press Ctrl+C to stop.");
|
|
31865
|
+
}
|
|
31866
|
+
var PORT = 54321;
|
|
31867
|
+
var init_listen = __esm(() => {
|
|
31868
|
+
init_source();
|
|
31869
|
+
init_network();
|
|
31870
|
+
init_ui();
|
|
31871
|
+
});
|
|
31872
|
+
|
|
31499
31873
|
// src/core.ts
|
|
31500
31874
|
var DebugMode;
|
|
31501
31875
|
var init_core = __esm(() => {
|
|
@@ -31655,6 +32029,18 @@ async function run() {
|
|
|
31655
32029
|
await transferFile2(commandArgs[0], ctx);
|
|
31656
32030
|
}
|
|
31657
32031
|
break;
|
|
32032
|
+
case "scan":
|
|
32033
|
+
{
|
|
32034
|
+
const { scanLocal: scanLocal2 } = await Promise.resolve().then(() => (init_scan(), exports_scan));
|
|
32035
|
+
await scanLocal2(commandArgs, ctx);
|
|
32036
|
+
}
|
|
32037
|
+
break;
|
|
32038
|
+
case "listen":
|
|
32039
|
+
{
|
|
32040
|
+
const { listen: listen2 } = await Promise.resolve().then(() => (init_listen(), exports_listen));
|
|
32041
|
+
await listen2(commandArgs, ctx);
|
|
32042
|
+
}
|
|
32043
|
+
break;
|
|
31658
32044
|
case "help":
|
|
31659
32045
|
{
|
|
31660
32046
|
const { showHelp: showHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
|
package/package.json
CHANGED
package/src/cli/commands/help.ts
CHANGED
|
@@ -78,6 +78,41 @@ ${chalk.bold("Usage:")}
|
|
|
78
78
|
syncstuff version
|
|
79
79
|
syncstuff --version
|
|
80
80
|
syncstuff -v`,
|
|
81
|
+
|
|
82
|
+
scan: `${chalk.cyan.bold("scan")}
|
|
83
|
+
|
|
84
|
+
Scan local network for SyncStuff devices.
|
|
85
|
+
|
|
86
|
+
${chalk.bold("Usage:")}
|
|
87
|
+
syncstuff scan [options]
|
|
88
|
+
|
|
89
|
+
${chalk.bold("Options:")}
|
|
90
|
+
--timeout N Set scan timeout in seconds (default: 10)
|
|
91
|
+
--watch, -w Continuously scan and show new devices
|
|
92
|
+
|
|
93
|
+
${chalk.bold("Description:")}
|
|
94
|
+
Discovers SyncStuff devices on your local network using UDP broadcast.
|
|
95
|
+
This works without requiring cloud authentication.
|
|
96
|
+
|
|
97
|
+
${chalk.bold("Examples:")}
|
|
98
|
+
syncstuff scan Scan for 10 seconds
|
|
99
|
+
syncstuff scan --timeout 30 Scan for 30 seconds
|
|
100
|
+
syncstuff scan --watch Continuously monitor for devices`,
|
|
101
|
+
|
|
102
|
+
listen: `${chalk.cyan.bold("listen")}
|
|
103
|
+
|
|
104
|
+
Start listening for incoming file transfers.
|
|
105
|
+
|
|
106
|
+
${chalk.bold("Usage:")}
|
|
107
|
+
syncstuff listen
|
|
108
|
+
|
|
109
|
+
${chalk.bold("Description:")}
|
|
110
|
+
Starts a local HTTP server and advertises this CLI on the network.
|
|
111
|
+
Allows other SyncStuff devices (like the Android app) to discover
|
|
112
|
+
this computer and send files to it directly via local network.
|
|
113
|
+
|
|
114
|
+
${chalk.bold("Examples:")}
|
|
115
|
+
syncstuff listen`,
|
|
81
116
|
};
|
|
82
117
|
|
|
83
118
|
export async function showHelp(command?: string): Promise<void> {
|
|
@@ -120,6 +155,9 @@ export async function showHelp(command?: string): Promise<void> {
|
|
|
120
155
|
chalk.green("device <id>") +
|
|
121
156
|
" Connect to a specific device\n" +
|
|
122
157
|
" " +
|
|
158
|
+
chalk.green("scan") +
|
|
159
|
+
" Scan local network for devices\n" +
|
|
160
|
+
" " +
|
|
123
161
|
chalk.green("transfer <file>") +
|
|
124
162
|
" Transfer a file to a device\n\n" +
|
|
125
163
|
chalk.bold("General:\n") +
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "http";
|
|
3
|
+
import { networkInterfaces } from "os";
|
|
4
|
+
import { type CommandContext, debugLog } from "../../utils/context.js";
|
|
5
|
+
import { networkScanner } from "../../utils/network.js"; // Added this line
|
|
6
|
+
import {
|
|
7
|
+
createSpinner,
|
|
8
|
+
error,
|
|
9
|
+
info,
|
|
10
|
+
printHeader,
|
|
11
|
+
success,
|
|
12
|
+
} from "../../utils/ui.js";
|
|
13
|
+
|
|
14
|
+
const PORT = 54321; // Default port for SyncStuff CLI
|
|
15
|
+
|
|
16
|
+
export async function listen(
|
|
17
|
+
_args: string[],
|
|
18
|
+
ctx: CommandContext,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
printHeader();
|
|
21
|
+
debugLog(ctx, "Starting listen command");
|
|
22
|
+
|
|
23
|
+
console.log(chalk.cyan("Starting SyncStuff CLI Listener..."));
|
|
24
|
+
console.log(
|
|
25
|
+
chalk.gray(
|
|
26
|
+
"This allows other devices to discover this CLI and send files directly.\n",
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const spinner = createSpinner("Initializing network listener...");
|
|
31
|
+
spinner.start();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// 1. Start HTTP Server
|
|
35
|
+
const server = createServer(handleRequest);
|
|
36
|
+
|
|
37
|
+
server.listen(PORT, "0.0.0.0", () => {
|
|
38
|
+
spinner.succeed(`HTTP Server running on port ${PORT}`);
|
|
39
|
+
|
|
40
|
+
const ips = getLocalIPs();
|
|
41
|
+
if (ips.length > 0) {
|
|
42
|
+
info("Listening on:");
|
|
43
|
+
ips.forEach(ip => {
|
|
44
|
+
console.log(` http://${ip}:${PORT}`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Start UDP Broadcast (Advertising)
|
|
49
|
+
startAdvertising();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.on("error", err => {
|
|
53
|
+
spinner.fail("Failed to start server");
|
|
54
|
+
error(`Server error: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
spinner.fail("Failed to start listener");
|
|
59
|
+
error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle incoming HTTP requests
|
|
66
|
+
*/
|
|
67
|
+
function handleRequest(req: IncomingMessage, res: ServerResponse) {
|
|
68
|
+
// CORS headers
|
|
69
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
70
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
71
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
72
|
+
|
|
73
|
+
if (req.method === "OPTIONS") {
|
|
74
|
+
res.writeHead(200);
|
|
75
|
+
res.end();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (req.method === "GET" && req.url === "/status") {
|
|
80
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(
|
|
82
|
+
JSON.stringify({ status: "online", type: "cli", version: "0.0.6" }),
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (req.method === "POST" && req.url === "/upload") {
|
|
88
|
+
// Handle file upload
|
|
89
|
+
info(`\nIncoming transfer request from ${req.socket.remoteAddress}`);
|
|
90
|
+
|
|
91
|
+
// Minimal handler for now - just acknowledge
|
|
92
|
+
// In a full implementation, we'd parse multipart/form-data or raw stream
|
|
93
|
+
// and save to disk
|
|
94
|
+
|
|
95
|
+
let dataLength = 0;
|
|
96
|
+
req.on("data", chunk => {
|
|
97
|
+
dataLength += chunk.length;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
req.on("end", () => {
|
|
101
|
+
success(`Received ${dataLength} bytes`);
|
|
102
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
103
|
+
res.end(JSON.stringify({ success: true, message: "File received" }));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.writeHead(404);
|
|
110
|
+
res.end("Not Found");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get local IP addresses
|
|
115
|
+
*/
|
|
116
|
+
function getLocalIPs(): string[] {
|
|
117
|
+
const nets = networkInterfaces();
|
|
118
|
+
const results: string[] = [];
|
|
119
|
+
|
|
120
|
+
for (const name of Object.keys(nets)) {
|
|
121
|
+
for (const net of nets[name] || []) {
|
|
122
|
+
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
|
123
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
124
|
+
results.push(net.address);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Advertise presence via UDP
|
|
133
|
+
*/
|
|
134
|
+
function startAdvertising() {
|
|
135
|
+
info("\nAdvertising CLI on local network...");
|
|
136
|
+
|
|
137
|
+
// Start advertising
|
|
138
|
+
// We use a random ID for the CLI session for now
|
|
139
|
+
networkScanner.startAdvertising("SyncStuff CLI", PORT);
|
|
140
|
+
|
|
141
|
+
info("Ready to receive files. Press Ctrl+C to stop.");
|
|
142
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { debugLog, type CommandContext } from "../../utils/context.js";
|
|
3
|
+
import { networkScanner, type LocalDevice } from "../../utils/network.js";
|
|
4
|
+
import {
|
|
5
|
+
createSpinner,
|
|
6
|
+
createTable,
|
|
7
|
+
error,
|
|
8
|
+
info,
|
|
9
|
+
printHeader,
|
|
10
|
+
printSeparator,
|
|
11
|
+
success,
|
|
12
|
+
warning,
|
|
13
|
+
} from "../../utils/ui.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan local network for SyncStuff devices
|
|
17
|
+
* This works without cloud authentication
|
|
18
|
+
*/
|
|
19
|
+
export async function scanLocal(
|
|
20
|
+
args: string[],
|
|
21
|
+
ctx: CommandContext,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const timeout = args.includes("--timeout")
|
|
24
|
+
? parseInt(args[args.indexOf("--timeout") + 1] || "10", 10)
|
|
25
|
+
: 10;
|
|
26
|
+
const continuous = args.includes("--watch") || args.includes("-w");
|
|
27
|
+
|
|
28
|
+
printHeader();
|
|
29
|
+
debugLog(ctx, "Scan command", { timeout, continuous });
|
|
30
|
+
|
|
31
|
+
console.log(chalk.cyan("Scanning local network for SyncStuff devices..."));
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.gray(`Timeout: ${timeout} seconds. Use --timeout N to change.\n`),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (continuous) {
|
|
37
|
+
await continuousScan(ctx, timeout);
|
|
38
|
+
} else {
|
|
39
|
+
await singleScan(ctx, timeout);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function singleScan(ctx: CommandContext, timeout: number): Promise<void> {
|
|
44
|
+
const spinner = createSpinner("Scanning network...");
|
|
45
|
+
spinner.start();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
49
|
+
|
|
50
|
+
if (devices.length === 0) {
|
|
51
|
+
spinner.info("No devices found on local network");
|
|
52
|
+
printSeparator();
|
|
53
|
+
info("Make sure SyncStuff is running on other devices on this network.");
|
|
54
|
+
info("Devices must be on the same local network (Wi-Fi or Ethernet).");
|
|
55
|
+
printSeparator();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
spinner.succeed(`Found ${devices.length} device(s) on local network`);
|
|
60
|
+
printSeparator();
|
|
61
|
+
|
|
62
|
+
displayDevices(devices);
|
|
63
|
+
|
|
64
|
+
printSeparator();
|
|
65
|
+
success(`Scan complete. Found ${devices.length} device(s).`);
|
|
66
|
+
info("Use 'syncstuff transfer <file>' to send files to these devices.");
|
|
67
|
+
printSeparator();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
spinner.fail("Network scan failed");
|
|
70
|
+
error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
debugLog(ctx, "Scan error", { error: err });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function continuousScan(
|
|
76
|
+
ctx: CommandContext,
|
|
77
|
+
timeout: number,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
console.log(chalk.cyan("Watch mode enabled. Press Ctrl+C to exit.\n"));
|
|
80
|
+
|
|
81
|
+
// Handle Ctrl+C gracefully
|
|
82
|
+
process.on("SIGINT", () => {
|
|
83
|
+
console.log("\n");
|
|
84
|
+
info("Scan stopped by user");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const knownDevices = new Map<string, LocalDevice>();
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const spinner = createSpinner("Scanning...");
|
|
92
|
+
spinner.start();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
96
|
+
|
|
97
|
+
// Check for new devices
|
|
98
|
+
for (const device of devices) {
|
|
99
|
+
if (!knownDevices.has(device.id)) {
|
|
100
|
+
spinner.succeed(`New device found: ${chalk.cyan(device.name)}`);
|
|
101
|
+
knownDevices.set(device.id, device);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for lost devices
|
|
106
|
+
for (const [id, device] of knownDevices.entries()) {
|
|
107
|
+
if (!devices.find(d => d.id === id)) {
|
|
108
|
+
warning(`Device lost: ${device.name}`);
|
|
109
|
+
knownDevices.delete(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (devices.length > 0) {
|
|
114
|
+
spinner.stop();
|
|
115
|
+
console.clear();
|
|
116
|
+
printHeader();
|
|
117
|
+
console.log(
|
|
118
|
+
chalk.cyan(
|
|
119
|
+
`Watch mode - ${new Date().toLocaleTimeString()} - Press Ctrl+C to exit.\n`,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
displayDevices(devices);
|
|
123
|
+
} else {
|
|
124
|
+
spinner.info("No devices found. Retrying...");
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
spinner.fail("Scan error, retrying...");
|
|
128
|
+
debugLog(ctx, "Watch scan error", { error: err });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Wait before next scan
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function displayDevices(devices: LocalDevice[]): void {
|
|
137
|
+
const tableData = devices.map(device => [
|
|
138
|
+
device.id.substring(0, 8) + "...",
|
|
139
|
+
device.name,
|
|
140
|
+
device.platform,
|
|
141
|
+
`${device.ip}:${device.port}`,
|
|
142
|
+
device.version,
|
|
143
|
+
chalk.green("Available"),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const table = createTable(tableData, [
|
|
147
|
+
"ID",
|
|
148
|
+
"Name",
|
|
149
|
+
"Platform",
|
|
150
|
+
"Address",
|
|
151
|
+
"Version",
|
|
152
|
+
"Status",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
console.log(table);
|
|
156
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -66,6 +66,18 @@ export async function run() {
|
|
|
66
66
|
await transferFile(commandArgs[0], ctx);
|
|
67
67
|
}
|
|
68
68
|
break;
|
|
69
|
+
case "scan":
|
|
70
|
+
{
|
|
71
|
+
const { scanLocal } = await import("./commands/scan.js");
|
|
72
|
+
await scanLocal(commandArgs, ctx);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case "listen":
|
|
76
|
+
{
|
|
77
|
+
const { listen } = await import("./commands/listen.js");
|
|
78
|
+
await listen(commandArgs, ctx);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
69
81
|
case "help":
|
|
70
82
|
{
|
|
71
83
|
const { showHelp } = await import("./commands/help.js");
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createSocket, type Socket } from "dgram";
|
|
2
|
+
import { networkInterfaces } from "os";
|
|
3
|
+
|
|
4
|
+
export interface LocalDevice {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
platform: string;
|
|
8
|
+
ip: string;
|
|
9
|
+
port: number;
|
|
10
|
+
version: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SYNCSTUFF_PORT = 5353; // mDNS port
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Network scanner for discovering SyncStuff devices on local network
|
|
17
|
+
* Uses UDP multicast to find devices
|
|
18
|
+
*/
|
|
19
|
+
class NetworkScanner {
|
|
20
|
+
private socket: Socket | null = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get local IP addresses
|
|
24
|
+
*/
|
|
25
|
+
getLocalIPs(): string[] {
|
|
26
|
+
const interfaces = networkInterfaces();
|
|
27
|
+
const ips: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const name of Object.keys(interfaces)) {
|
|
30
|
+
const iface = interfaces[name];
|
|
31
|
+
if (!iface) continue;
|
|
32
|
+
|
|
33
|
+
for (const addr of iface) {
|
|
34
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
35
|
+
ips.push(addr.address);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ips;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan the local network for SyncStuff devices
|
|
45
|
+
* Uses UDP broadcast to discover devices
|
|
46
|
+
*/
|
|
47
|
+
async scan(timeout = 10000): Promise<LocalDevice[]> {
|
|
48
|
+
return new Promise(resolve => {
|
|
49
|
+
const devices: LocalDevice[] = [];
|
|
50
|
+
const seenIds = new Set<string>();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
this.socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
54
|
+
|
|
55
|
+
this.socket.on("error", err => {
|
|
56
|
+
console.error("Scanner error:", err.message);
|
|
57
|
+
this.cleanup();
|
|
58
|
+
resolve(devices);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(msg.toString());
|
|
64
|
+
|
|
65
|
+
if (data.service === "syncstuff" && data.deviceId) {
|
|
66
|
+
if (!seenIds.has(data.deviceId)) {
|
|
67
|
+
seenIds.add(data.deviceId);
|
|
68
|
+
devices.push({
|
|
69
|
+
id: data.deviceId,
|
|
70
|
+
name: data.deviceName || "Unknown Device",
|
|
71
|
+
platform: data.platform || "unknown",
|
|
72
|
+
ip: rinfo.address,
|
|
73
|
+
port: data.port || 8080,
|
|
74
|
+
version: data.version || "1.0.0",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore non-JSON messages
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.socket.bind(0, () => {
|
|
84
|
+
this.socket?.setBroadcast(true);
|
|
85
|
+
|
|
86
|
+
// Send discovery broadcast
|
|
87
|
+
const discoveryMessage = JSON.stringify({
|
|
88
|
+
service: "syncstuff",
|
|
89
|
+
action: "discover",
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Broadcast on local network
|
|
94
|
+
const localIPs = this.getLocalIPs();
|
|
95
|
+
for (const ip of localIPs) {
|
|
96
|
+
const parts = ip.split(".");
|
|
97
|
+
parts[3] = "255";
|
|
98
|
+
const broadcastAddr = parts.join(".");
|
|
99
|
+
|
|
100
|
+
this.socket?.send(
|
|
101
|
+
discoveryMessage,
|
|
102
|
+
0,
|
|
103
|
+
discoveryMessage.length,
|
|
104
|
+
SYNCSTUFF_PORT,
|
|
105
|
+
broadcastAddr,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Also try mDNS multicast address
|
|
110
|
+
this.socket?.send(
|
|
111
|
+
discoveryMessage,
|
|
112
|
+
0,
|
|
113
|
+
discoveryMessage.length,
|
|
114
|
+
SYNCSTUFF_PORT,
|
|
115
|
+
"224.0.0.251",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Cleanup after timeout
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
this.cleanup();
|
|
122
|
+
resolve(devices);
|
|
123
|
+
}, timeout);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("Failed to start scanner:", error);
|
|
126
|
+
resolve(devices);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private cleanup(): void {
|
|
132
|
+
if (this.socket) {
|
|
133
|
+
try {
|
|
134
|
+
this.socket.close();
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore close errors
|
|
137
|
+
}
|
|
138
|
+
this.socket = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Send a message to a specific device
|
|
144
|
+
*/
|
|
145
|
+
async sendTo(ip: string, port: number, message: object): Promise<void> {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const socket = createSocket("udp4");
|
|
148
|
+
const data = JSON.stringify(message);
|
|
149
|
+
|
|
150
|
+
socket.send(data, 0, data.length, port, ip, err => {
|
|
151
|
+
socket.close();
|
|
152
|
+
if (err) {
|
|
153
|
+
reject(err);
|
|
154
|
+
} else {
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Start advertising this device on the local network
|
|
163
|
+
*/
|
|
164
|
+
startAdvertising(
|
|
165
|
+
deviceName: string,
|
|
166
|
+
port: number,
|
|
167
|
+
platform = "cli",
|
|
168
|
+
): NodeJS.Timer {
|
|
169
|
+
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
170
|
+
|
|
171
|
+
socket.bind(() => {
|
|
172
|
+
socket.setBroadcast(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const advertise = () => {
|
|
176
|
+
const message = JSON.stringify({
|
|
177
|
+
service: "syncstuff",
|
|
178
|
+
deviceId: "cli-device-" + Math.floor(Math.random() * 10000), // Simple random ID for CLI
|
|
179
|
+
deviceName,
|
|
180
|
+
platform,
|
|
181
|
+
port,
|
|
182
|
+
version: "0.0.6", // Should match package.json
|
|
183
|
+
timestamp: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const localIPs = this.getLocalIPs();
|
|
187
|
+
for (const ip of localIPs) {
|
|
188
|
+
const parts = ip.split(".");
|
|
189
|
+
parts[3] = "255";
|
|
190
|
+
const broadcastAddr = parts.join(".");
|
|
191
|
+
|
|
192
|
+
socket.send(message, 0, message.length, SYNCSTUFF_PORT, broadcastAddr);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Also send to multicast group
|
|
196
|
+
socket.send(message, 0, message.length, SYNCSTUFF_PORT, "224.0.0.251");
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Advertise immediately
|
|
200
|
+
advertise();
|
|
201
|
+
|
|
202
|
+
// Then every 3 seconds
|
|
203
|
+
return setInterval(advertise, 3000);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const networkScanner = new NetworkScanner();
|