@involvex/syncstuff-cli 0.0.4 → 0.0.6
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 +353 -23
- package/package.json +1 -1
- package/src/cli/commands/devices.ts +71 -4
- package/src/cli/commands/help.ts +23 -0
- package/src/cli/commands/scan.ts +156 -0
- package/src/cli/commands/version.ts +0 -1
- package/src/cli/index.ts +11 -1
- package/src/utils/network.ts +162 -0
- package/src/utils/update-checker.ts +86 -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,26 @@ 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`
|
|
9791
9811
|
};
|
|
9792
9812
|
});
|
|
9793
9813
|
|
|
@@ -20411,7 +20431,7 @@ var init_RemoveFileError = __esm(() => {
|
|
|
20411
20431
|
|
|
20412
20432
|
// ../../node_modules/.bun/@inquirer+external-editor@2.0.2+cda980ff03d0f389/node_modules/@inquirer/external-editor/dist/index.js
|
|
20413
20433
|
import { spawn, spawnSync } from "child_process";
|
|
20414
|
-
import { readFileSync as
|
|
20434
|
+
import { readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
|
|
20415
20435
|
import path from "node:path";
|
|
20416
20436
|
import os2 from "node:os";
|
|
20417
20437
|
import { randomUUID } from "node:crypto";
|
|
@@ -20527,7 +20547,7 @@ class ExternalEditor {
|
|
|
20527
20547
|
}
|
|
20528
20548
|
readTemporaryFile() {
|
|
20529
20549
|
try {
|
|
20530
|
-
const tempFileBuffer =
|
|
20550
|
+
const tempFileBuffer = readFileSync3(this.tempFile);
|
|
20531
20551
|
if (tempFileBuffer.length === 0) {
|
|
20532
20552
|
this.text = "";
|
|
20533
20553
|
} else {
|
|
@@ -30801,9 +30821,9 @@ var init_dist16 = __esm(() => {
|
|
|
30801
30821
|
});
|
|
30802
30822
|
|
|
30803
30823
|
// src/utils/config.ts
|
|
30804
|
-
import { mkdirSync, readFileSync as
|
|
30824
|
+
import { mkdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
30805
30825
|
import { homedir } from "os";
|
|
30806
|
-
import { join as
|
|
30826
|
+
import { join as join3 } from "path";
|
|
30807
30827
|
function ensureConfigDir() {
|
|
30808
30828
|
try {
|
|
30809
30829
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -30812,7 +30832,7 @@ function ensureConfigDir() {
|
|
|
30812
30832
|
function readConfig() {
|
|
30813
30833
|
ensureConfigDir();
|
|
30814
30834
|
try {
|
|
30815
|
-
const content =
|
|
30835
|
+
const content = readFileSync4(CONFIG_FILE, "utf-8");
|
|
30816
30836
|
return JSON.parse(content);
|
|
30817
30837
|
} catch {
|
|
30818
30838
|
return {};
|
|
@@ -30824,8 +30844,8 @@ function writeConfig(config) {
|
|
|
30824
30844
|
}
|
|
30825
30845
|
var CONFIG_DIR, CONFIG_FILE;
|
|
30826
30846
|
var init_config = __esm(() => {
|
|
30827
|
-
CONFIG_DIR =
|
|
30828
|
-
CONFIG_FILE =
|
|
30847
|
+
CONFIG_DIR = join3(homedir(), ".syncstuff");
|
|
30848
|
+
CONFIG_FILE = join3(CONFIG_DIR, "config.json");
|
|
30829
30849
|
});
|
|
30830
30850
|
|
|
30831
30851
|
// src/utils/api-client.ts
|
|
@@ -31079,8 +31099,17 @@ var exports_devices = {};
|
|
|
31079
31099
|
__export(exports_devices, {
|
|
31080
31100
|
listDevices: () => listDevices
|
|
31081
31101
|
});
|
|
31082
|
-
async function listDevices(ctx) {
|
|
31083
|
-
|
|
31102
|
+
async function listDevices(args, ctx) {
|
|
31103
|
+
const isLoop = args.includes("--loop") || args.includes("-l");
|
|
31104
|
+
debugLog(ctx, "Devices command", { loop: isLoop });
|
|
31105
|
+
if (isLoop) {
|
|
31106
|
+
await loopDevices(ctx);
|
|
31107
|
+
} else {
|
|
31108
|
+
printHeader();
|
|
31109
|
+
await fetchAndDisplayDevices(ctx);
|
|
31110
|
+
}
|
|
31111
|
+
}
|
|
31112
|
+
async function fetchAndDisplayDevices(ctx) {
|
|
31084
31113
|
debugLog(ctx, "Fetching devices list");
|
|
31085
31114
|
if (!apiClient.isAuthenticated()) {
|
|
31086
31115
|
error2("You are not logged in. Please run 'syncstuff login' first.");
|
|
@@ -31096,7 +31125,7 @@ async function listDevices(ctx) {
|
|
|
31096
31125
|
if (response.data.length === 0) {
|
|
31097
31126
|
info2("No devices found. Connect a device to get started.");
|
|
31098
31127
|
printSeparator();
|
|
31099
|
-
return;
|
|
31128
|
+
return true;
|
|
31100
31129
|
}
|
|
31101
31130
|
const tableData = response.data.map((device) => [
|
|
31102
31131
|
device.id.substring(0, 8) + "...",
|
|
@@ -31118,6 +31147,7 @@ async function listDevices(ctx) {
|
|
|
31118
31147
|
printSeparator();
|
|
31119
31148
|
success2(`Found ${response.data.length} device(s)`);
|
|
31120
31149
|
printSeparator();
|
|
31150
|
+
return true;
|
|
31121
31151
|
} else {
|
|
31122
31152
|
spinner.fail("Failed to fetch devices");
|
|
31123
31153
|
if (response.error?.includes("404") || response.error?.includes("Not found")) {
|
|
@@ -31126,13 +31156,39 @@ async function listDevices(ctx) {
|
|
|
31126
31156
|
} else {
|
|
31127
31157
|
error2(response.error || "Unknown error");
|
|
31128
31158
|
}
|
|
31159
|
+
return false;
|
|
31129
31160
|
}
|
|
31130
31161
|
} catch (err) {
|
|
31131
31162
|
spinner.fail("Error fetching devices");
|
|
31132
31163
|
error2(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31133
|
-
|
|
31164
|
+
return false;
|
|
31134
31165
|
}
|
|
31135
31166
|
}
|
|
31167
|
+
async function loopDevices(ctx) {
|
|
31168
|
+
const REFRESH_INTERVAL = 5000;
|
|
31169
|
+
console.clear();
|
|
31170
|
+
printHeader();
|
|
31171
|
+
console.log(source_default.cyan("Loop mode enabled. Press Ctrl+C to exit."));
|
|
31172
|
+
console.log(source_default.gray(`Refreshing every ${REFRESH_INTERVAL / 1000} seconds...
|
|
31173
|
+
`));
|
|
31174
|
+
process.on("SIGINT", () => {
|
|
31175
|
+
console.log(`
|
|
31176
|
+
`);
|
|
31177
|
+
info2("Loop stopped by user");
|
|
31178
|
+
process.exit(0);
|
|
31179
|
+
});
|
|
31180
|
+
await fetchAndDisplayDevices(ctx);
|
|
31181
|
+
const interval = setInterval(async () => {
|
|
31182
|
+
console.clear();
|
|
31183
|
+
printHeader();
|
|
31184
|
+
console.log(source_default.cyan("Loop mode enabled. Press Ctrl+C to exit."));
|
|
31185
|
+
console.log(source_default.gray(`Last refresh: ${new Date().toLocaleTimeString()}
|
|
31186
|
+
`));
|
|
31187
|
+
await fetchAndDisplayDevices(ctx);
|
|
31188
|
+
}, REFRESH_INTERVAL);
|
|
31189
|
+
await new Promise(() => {});
|
|
31190
|
+
clearInterval(interval);
|
|
31191
|
+
}
|
|
31136
31192
|
var init_devices = __esm(() => {
|
|
31137
31193
|
init_source();
|
|
31138
31194
|
init_api_client();
|
|
@@ -31460,6 +31516,224 @@ var init_transfer = __esm(() => {
|
|
|
31460
31516
|
init_ui();
|
|
31461
31517
|
});
|
|
31462
31518
|
|
|
31519
|
+
// src/utils/network.ts
|
|
31520
|
+
import { createSocket } from "dgram";
|
|
31521
|
+
import { networkInterfaces } from "os";
|
|
31522
|
+
|
|
31523
|
+
class NetworkScanner {
|
|
31524
|
+
socket = null;
|
|
31525
|
+
getLocalIPs() {
|
|
31526
|
+
const interfaces = networkInterfaces();
|
|
31527
|
+
const ips = [];
|
|
31528
|
+
for (const name of Object.keys(interfaces)) {
|
|
31529
|
+
const iface = interfaces[name];
|
|
31530
|
+
if (!iface)
|
|
31531
|
+
continue;
|
|
31532
|
+
for (const addr of iface) {
|
|
31533
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
31534
|
+
ips.push(addr.address);
|
|
31535
|
+
}
|
|
31536
|
+
}
|
|
31537
|
+
}
|
|
31538
|
+
return ips;
|
|
31539
|
+
}
|
|
31540
|
+
async scan(timeout = 1e4) {
|
|
31541
|
+
return new Promise((resolve2) => {
|
|
31542
|
+
const devices = [];
|
|
31543
|
+
const seenIds = new Set;
|
|
31544
|
+
try {
|
|
31545
|
+
this.socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
31546
|
+
this.socket.on("error", (err) => {
|
|
31547
|
+
console.error("Scanner error:", err.message);
|
|
31548
|
+
this.cleanup();
|
|
31549
|
+
resolve2(devices);
|
|
31550
|
+
});
|
|
31551
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
31552
|
+
try {
|
|
31553
|
+
const data = JSON.parse(msg.toString());
|
|
31554
|
+
if (data.service === "syncstuff" && data.deviceId) {
|
|
31555
|
+
if (!seenIds.has(data.deviceId)) {
|
|
31556
|
+
seenIds.add(data.deviceId);
|
|
31557
|
+
devices.push({
|
|
31558
|
+
id: data.deviceId,
|
|
31559
|
+
name: data.deviceName || "Unknown Device",
|
|
31560
|
+
platform: data.platform || "unknown",
|
|
31561
|
+
ip: rinfo.address,
|
|
31562
|
+
port: data.port || 8080,
|
|
31563
|
+
version: data.version || "1.0.0"
|
|
31564
|
+
});
|
|
31565
|
+
}
|
|
31566
|
+
}
|
|
31567
|
+
} catch {}
|
|
31568
|
+
});
|
|
31569
|
+
this.socket.bind(0, () => {
|
|
31570
|
+
this.socket?.setBroadcast(true);
|
|
31571
|
+
const discoveryMessage = JSON.stringify({
|
|
31572
|
+
service: "syncstuff",
|
|
31573
|
+
action: "discover",
|
|
31574
|
+
timestamp: Date.now()
|
|
31575
|
+
});
|
|
31576
|
+
const localIPs = this.getLocalIPs();
|
|
31577
|
+
for (const ip of localIPs) {
|
|
31578
|
+
const parts = ip.split(".");
|
|
31579
|
+
parts[3] = "255";
|
|
31580
|
+
const broadcastAddr = parts.join(".");
|
|
31581
|
+
this.socket?.send(discoveryMessage, 0, discoveryMessage.length, SYNCSTUFF_PORT, broadcastAddr);
|
|
31582
|
+
}
|
|
31583
|
+
this.socket?.send(discoveryMessage, 0, discoveryMessage.length, SYNCSTUFF_PORT, "224.0.0.251");
|
|
31584
|
+
});
|
|
31585
|
+
setTimeout(() => {
|
|
31586
|
+
this.cleanup();
|
|
31587
|
+
resolve2(devices);
|
|
31588
|
+
}, timeout);
|
|
31589
|
+
} catch (error3) {
|
|
31590
|
+
console.error("Failed to start scanner:", error3);
|
|
31591
|
+
resolve2(devices);
|
|
31592
|
+
}
|
|
31593
|
+
});
|
|
31594
|
+
}
|
|
31595
|
+
cleanup() {
|
|
31596
|
+
if (this.socket) {
|
|
31597
|
+
try {
|
|
31598
|
+
this.socket.close();
|
|
31599
|
+
} catch {}
|
|
31600
|
+
this.socket = null;
|
|
31601
|
+
}
|
|
31602
|
+
}
|
|
31603
|
+
async sendTo(ip, port, message) {
|
|
31604
|
+
return new Promise((resolve2, reject) => {
|
|
31605
|
+
const socket = createSocket("udp4");
|
|
31606
|
+
const data = JSON.stringify(message);
|
|
31607
|
+
socket.send(data, 0, data.length, port, ip, (err) => {
|
|
31608
|
+
socket.close();
|
|
31609
|
+
if (err) {
|
|
31610
|
+
reject(err);
|
|
31611
|
+
} else {
|
|
31612
|
+
resolve2();
|
|
31613
|
+
}
|
|
31614
|
+
});
|
|
31615
|
+
});
|
|
31616
|
+
}
|
|
31617
|
+
}
|
|
31618
|
+
var SYNCSTUFF_PORT = 5353, networkScanner;
|
|
31619
|
+
var init_network = __esm(() => {
|
|
31620
|
+
networkScanner = new NetworkScanner;
|
|
31621
|
+
});
|
|
31622
|
+
|
|
31623
|
+
// src/cli/commands/scan.ts
|
|
31624
|
+
var exports_scan = {};
|
|
31625
|
+
__export(exports_scan, {
|
|
31626
|
+
scanLocal: () => scanLocal
|
|
31627
|
+
});
|
|
31628
|
+
async function scanLocal(args, ctx) {
|
|
31629
|
+
const timeout = args.includes("--timeout") ? parseInt(args[args.indexOf("--timeout") + 1] || "10", 10) : 10;
|
|
31630
|
+
const continuous = args.includes("--watch") || args.includes("-w");
|
|
31631
|
+
printHeader();
|
|
31632
|
+
debugLog(ctx, "Scan command", { timeout, continuous });
|
|
31633
|
+
console.log(source_default.cyan("Scanning local network for SyncStuff devices..."));
|
|
31634
|
+
console.log(source_default.gray(`Timeout: ${timeout} seconds. Use --timeout N to change.
|
|
31635
|
+
`));
|
|
31636
|
+
if (continuous) {
|
|
31637
|
+
await continuousScan(ctx, timeout);
|
|
31638
|
+
} else {
|
|
31639
|
+
await singleScan(ctx, timeout);
|
|
31640
|
+
}
|
|
31641
|
+
}
|
|
31642
|
+
async function singleScan(ctx, timeout) {
|
|
31643
|
+
const spinner = createSpinner("Scanning network...");
|
|
31644
|
+
spinner.start();
|
|
31645
|
+
try {
|
|
31646
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
31647
|
+
if (devices.length === 0) {
|
|
31648
|
+
spinner.info("No devices found on local network");
|
|
31649
|
+
printSeparator();
|
|
31650
|
+
info2("Make sure SyncStuff is running on other devices on this network.");
|
|
31651
|
+
info2("Devices must be on the same local network (Wi-Fi or Ethernet).");
|
|
31652
|
+
printSeparator();
|
|
31653
|
+
return;
|
|
31654
|
+
}
|
|
31655
|
+
spinner.succeed(`Found ${devices.length} device(s) on local network`);
|
|
31656
|
+
printSeparator();
|
|
31657
|
+
displayDevices(devices);
|
|
31658
|
+
printSeparator();
|
|
31659
|
+
success2(`Scan complete. Found ${devices.length} device(s).`);
|
|
31660
|
+
info2("Use 'syncstuff transfer <file>' to send files to these devices.");
|
|
31661
|
+
printSeparator();
|
|
31662
|
+
} catch (err) {
|
|
31663
|
+
spinner.fail("Network scan failed");
|
|
31664
|
+
error2(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
31665
|
+
debugLog(ctx, "Scan error", { error: err });
|
|
31666
|
+
}
|
|
31667
|
+
}
|
|
31668
|
+
async function continuousScan(ctx, timeout) {
|
|
31669
|
+
console.log(source_default.cyan(`Watch mode enabled. Press Ctrl+C to exit.
|
|
31670
|
+
`));
|
|
31671
|
+
process.on("SIGINT", () => {
|
|
31672
|
+
console.log(`
|
|
31673
|
+
`);
|
|
31674
|
+
info2("Scan stopped by user");
|
|
31675
|
+
process.exit(0);
|
|
31676
|
+
});
|
|
31677
|
+
const knownDevices = new Map;
|
|
31678
|
+
while (true) {
|
|
31679
|
+
const spinner = createSpinner("Scanning...");
|
|
31680
|
+
spinner.start();
|
|
31681
|
+
try {
|
|
31682
|
+
const devices = await networkScanner.scan(timeout * 1000);
|
|
31683
|
+
for (const device2 of devices) {
|
|
31684
|
+
if (!knownDevices.has(device2.id)) {
|
|
31685
|
+
spinner.succeed(`New device found: ${source_default.cyan(device2.name)}`);
|
|
31686
|
+
knownDevices.set(device2.id, device2);
|
|
31687
|
+
}
|
|
31688
|
+
}
|
|
31689
|
+
for (const [id, device2] of knownDevices.entries()) {
|
|
31690
|
+
if (!devices.find((d) => d.id === id)) {
|
|
31691
|
+
warning2(`Device lost: ${device2.name}`);
|
|
31692
|
+
knownDevices.delete(id);
|
|
31693
|
+
}
|
|
31694
|
+
}
|
|
31695
|
+
if (devices.length > 0) {
|
|
31696
|
+
spinner.stop();
|
|
31697
|
+
console.clear();
|
|
31698
|
+
printHeader();
|
|
31699
|
+
console.log(source_default.cyan(`Watch mode - ${new Date().toLocaleTimeString()} - Press Ctrl+C to exit.
|
|
31700
|
+
`));
|
|
31701
|
+
displayDevices(devices);
|
|
31702
|
+
} else {
|
|
31703
|
+
spinner.info("No devices found. Retrying...");
|
|
31704
|
+
}
|
|
31705
|
+
} catch (err) {
|
|
31706
|
+
spinner.fail("Scan error, retrying...");
|
|
31707
|
+
debugLog(ctx, "Watch scan error", { error: err });
|
|
31708
|
+
}
|
|
31709
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3000));
|
|
31710
|
+
}
|
|
31711
|
+
}
|
|
31712
|
+
function displayDevices(devices) {
|
|
31713
|
+
const tableData = devices.map((device2) => [
|
|
31714
|
+
device2.id.substring(0, 8) + "...",
|
|
31715
|
+
device2.name,
|
|
31716
|
+
device2.platform,
|
|
31717
|
+
`${device2.ip}:${device2.port}`,
|
|
31718
|
+
device2.version,
|
|
31719
|
+
source_default.green("Available")
|
|
31720
|
+
]);
|
|
31721
|
+
const table = createTable(tableData, [
|
|
31722
|
+
"ID",
|
|
31723
|
+
"Name",
|
|
31724
|
+
"Platform",
|
|
31725
|
+
"Address",
|
|
31726
|
+
"Version",
|
|
31727
|
+
"Status"
|
|
31728
|
+
]);
|
|
31729
|
+
console.log(table);
|
|
31730
|
+
}
|
|
31731
|
+
var init_scan = __esm(() => {
|
|
31732
|
+
init_source();
|
|
31733
|
+
init_network();
|
|
31734
|
+
init_ui();
|
|
31735
|
+
});
|
|
31736
|
+
|
|
31463
31737
|
// src/core.ts
|
|
31464
31738
|
var DebugMode;
|
|
31465
31739
|
var init_core = __esm(() => {
|
|
@@ -31473,14 +31747,14 @@ var exports_version = {};
|
|
|
31473
31747
|
__export(exports_version, {
|
|
31474
31748
|
showversion: () => showversion
|
|
31475
31749
|
});
|
|
31476
|
-
import { readFileSync as
|
|
31477
|
-
import { dirname, join as
|
|
31478
|
-
import { fileURLToPath } from "url";
|
|
31750
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
31751
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
31752
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
31479
31753
|
function showversion() {
|
|
31480
31754
|
printHeader();
|
|
31481
31755
|
try {
|
|
31482
|
-
const packagePath =
|
|
31483
|
-
const packageJson = JSON.parse(
|
|
31756
|
+
const packagePath = join4(__dirname3, "../../../package.json");
|
|
31757
|
+
const packageJson = JSON.parse(readFileSync5(packagePath, "utf-8"));
|
|
31484
31758
|
const version = packageJson.version;
|
|
31485
31759
|
if (DebugMode.enabled === true) {
|
|
31486
31760
|
console.log(source_default.yellow("Debug mode enabled"));
|
|
@@ -31498,29 +31772,79 @@ function showversion() {
|
|
|
31498
31772
|
printSeparator();
|
|
31499
31773
|
} catch {
|
|
31500
31774
|
if (DebugMode.enabled === true) {
|
|
31501
|
-
const packagePath =
|
|
31775
|
+
const packagePath = join4(__dirname3, "../../../package.json");
|
|
31502
31776
|
console.log(source_default.yellow("Debug mode enabled"));
|
|
31503
31777
|
console.log("Packagepath: " + packagePath);
|
|
31504
31778
|
console.log(source_default.yellow("Version: 0.0.1 (unable to read package.json)"));
|
|
31505
31779
|
}
|
|
31506
|
-
console.log(source_default.yellow("Version: 0.0.1 (unable to read package.json)"));
|
|
31507
31780
|
}
|
|
31508
31781
|
}
|
|
31509
|
-
var
|
|
31782
|
+
var __filename3, __dirname3;
|
|
31510
31783
|
var init_version = __esm(() => {
|
|
31511
31784
|
init_source();
|
|
31512
31785
|
init_core();
|
|
31513
31786
|
init_ui();
|
|
31514
|
-
|
|
31515
|
-
|
|
31787
|
+
__filename3 = fileURLToPath2(import.meta.url);
|
|
31788
|
+
__dirname3 = dirname2(__filename3);
|
|
31516
31789
|
});
|
|
31517
31790
|
|
|
31518
31791
|
// src/cli/index.ts
|
|
31519
31792
|
init_ui();
|
|
31793
|
+
|
|
31794
|
+
// src/utils/update-checker.ts
|
|
31795
|
+
init_source();
|
|
31796
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
31797
|
+
import { dirname, join as join2 } from "path";
|
|
31798
|
+
import { fileURLToPath } from "url";
|
|
31799
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
31800
|
+
var __dirname2 = dirname(__filename2);
|
|
31801
|
+
async function checkForUpdates() {
|
|
31802
|
+
try {
|
|
31803
|
+
const packagePath = join2(__dirname2, "../../package.json");
|
|
31804
|
+
const packageJson = JSON.parse(readFileSync2(packagePath, "utf-8"));
|
|
31805
|
+
const currentVersion = packageJson.version;
|
|
31806
|
+
const packageName = packageJson.name;
|
|
31807
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
31808
|
+
headers: { Accept: "application/json" },
|
|
31809
|
+
signal: AbortSignal.timeout(3000)
|
|
31810
|
+
});
|
|
31811
|
+
if (!response.ok) {
|
|
31812
|
+
return;
|
|
31813
|
+
}
|
|
31814
|
+
const data = await response.json();
|
|
31815
|
+
const latestVersion = data.version;
|
|
31816
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
31817
|
+
const isNewer = compareVersions(latestVersion, currentVersion) > 0;
|
|
31818
|
+
if (isNewer) {
|
|
31819
|
+
console.log("");
|
|
31820
|
+
console.log(source_default.yellow.bold("╭─────────────────────────────────────────╮"));
|
|
31821
|
+
console.log(source_default.yellow.bold("│") + source_default.yellow(" Update available! ") + source_default.gray(`${currentVersion}`) + source_default.yellow(" → ") + source_default.green.bold(`${latestVersion}`) + source_default.yellow.bold(" │"));
|
|
31822
|
+
console.log(source_default.yellow.bold("│") + source_default.cyan(` Run: npm i -g ${packageName}`) + " ".repeat(7) + source_default.yellow.bold("│"));
|
|
31823
|
+
console.log(source_default.yellow.bold("╰─────────────────────────────────────────╯"));
|
|
31824
|
+
console.log("");
|
|
31825
|
+
}
|
|
31826
|
+
}
|
|
31827
|
+
} catch {}
|
|
31828
|
+
}
|
|
31829
|
+
function compareVersions(a, b) {
|
|
31830
|
+
const partsA = a.split(".").map(Number);
|
|
31831
|
+
const partsB = b.split(".").map(Number);
|
|
31832
|
+
for (let i = 0;i < 3; i++) {
|
|
31833
|
+
const partA = partsA[i] || 0;
|
|
31834
|
+
const partB = partsB[i] || 0;
|
|
31835
|
+
if (partA !== partB) {
|
|
31836
|
+
return partA - partB;
|
|
31837
|
+
}
|
|
31838
|
+
}
|
|
31839
|
+
return 0;
|
|
31840
|
+
}
|
|
31841
|
+
|
|
31842
|
+
// src/cli/index.ts
|
|
31520
31843
|
async function run() {
|
|
31521
31844
|
const args = process.argv.slice(2);
|
|
31522
31845
|
const { command, flags, commandArgs } = parseArgs(args);
|
|
31523
31846
|
const ctx = { debug: flags.debug };
|
|
31847
|
+
checkForUpdates();
|
|
31524
31848
|
debugLog(ctx, "Parsed arguments:", { command, flags, commandArgs });
|
|
31525
31849
|
if (flags.help) {
|
|
31526
31850
|
const { showHelp: showHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
|
|
@@ -31554,7 +31878,7 @@ async function run() {
|
|
|
31554
31878
|
case "devices":
|
|
31555
31879
|
{
|
|
31556
31880
|
const { listDevices: listDevices2 } = await Promise.resolve().then(() => (init_devices(), exports_devices));
|
|
31557
|
-
await listDevices2(ctx);
|
|
31881
|
+
await listDevices2(commandArgs, ctx);
|
|
31558
31882
|
}
|
|
31559
31883
|
break;
|
|
31560
31884
|
case "device":
|
|
@@ -31569,6 +31893,12 @@ async function run() {
|
|
|
31569
31893
|
await transferFile2(commandArgs[0], ctx);
|
|
31570
31894
|
}
|
|
31571
31895
|
break;
|
|
31896
|
+
case "scan":
|
|
31897
|
+
{
|
|
31898
|
+
const { scanLocal: scanLocal2 } = await Promise.resolve().then(() => (init_scan(), exports_scan));
|
|
31899
|
+
await scanLocal2(commandArgs, ctx);
|
|
31900
|
+
}
|
|
31901
|
+
break;
|
|
31572
31902
|
case "help":
|
|
31573
31903
|
{
|
|
31574
31904
|
const { showHelp: showHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
|
package/package.json
CHANGED
|
@@ -11,8 +11,31 @@ import {
|
|
|
11
11
|
success,
|
|
12
12
|
} from "../../utils/ui.js";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* List devices command
|
|
16
|
+
* Options:
|
|
17
|
+
* --loop Continuously refresh devices until Ctrl+C
|
|
18
|
+
*/
|
|
19
|
+
export async function listDevices(
|
|
20
|
+
args: string[],
|
|
21
|
+
ctx: CommandContext,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const isLoop = args.includes("--loop") || args.includes("-l");
|
|
24
|
+
|
|
25
|
+
debugLog(ctx, "Devices command", { loop: isLoop });
|
|
26
|
+
|
|
27
|
+
if (isLoop) {
|
|
28
|
+
await loopDevices(ctx);
|
|
29
|
+
} else {
|
|
30
|
+
printHeader();
|
|
31
|
+
await fetchAndDisplayDevices(ctx);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch and display devices once
|
|
37
|
+
*/
|
|
38
|
+
async function fetchAndDisplayDevices(ctx: CommandContext): Promise<boolean> {
|
|
16
39
|
debugLog(ctx, "Fetching devices list");
|
|
17
40
|
|
|
18
41
|
if (!apiClient.isAuthenticated()) {
|
|
@@ -33,7 +56,7 @@ export async function listDevices(ctx: CommandContext): Promise<void> {
|
|
|
33
56
|
if (response.data.length === 0) {
|
|
34
57
|
info("No devices found. Connect a device to get started.");
|
|
35
58
|
printSeparator();
|
|
36
|
-
return;
|
|
59
|
+
return true;
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
const tableData = response.data.map(device => [
|
|
@@ -58,6 +81,7 @@ export async function listDevices(ctx: CommandContext): Promise<void> {
|
|
|
58
81
|
printSeparator();
|
|
59
82
|
success(`Found ${response.data.length} device(s)`);
|
|
60
83
|
printSeparator();
|
|
84
|
+
return true;
|
|
61
85
|
} else {
|
|
62
86
|
spinner.fail("Failed to fetch devices");
|
|
63
87
|
if (
|
|
@@ -69,10 +93,53 @@ export async function listDevices(ctx: CommandContext): Promise<void> {
|
|
|
69
93
|
} else {
|
|
70
94
|
error(response.error || "Unknown error");
|
|
71
95
|
}
|
|
96
|
+
return false;
|
|
72
97
|
}
|
|
73
98
|
} catch (err) {
|
|
74
99
|
spinner.fail("Error fetching devices");
|
|
75
100
|
error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
76
|
-
|
|
101
|
+
return false;
|
|
77
102
|
}
|
|
78
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Loop mode: continuously refresh devices
|
|
107
|
+
*/
|
|
108
|
+
async function loopDevices(ctx: CommandContext): Promise<void> {
|
|
109
|
+
const REFRESH_INTERVAL = 5000; // 5 seconds
|
|
110
|
+
|
|
111
|
+
console.clear();
|
|
112
|
+
printHeader();
|
|
113
|
+
console.log(chalk.cyan("Loop mode enabled. Press Ctrl+C to exit."));
|
|
114
|
+
console.log(
|
|
115
|
+
chalk.gray(`Refreshing every ${REFRESH_INTERVAL / 1000} seconds...\n`),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Handle Ctrl+C gracefully
|
|
119
|
+
process.on("SIGINT", () => {
|
|
120
|
+
console.log("\n");
|
|
121
|
+
info("Loop stopped by user");
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Initial fetch
|
|
126
|
+
await fetchAndDisplayDevices(ctx);
|
|
127
|
+
|
|
128
|
+
// Set up refresh loop
|
|
129
|
+
const interval = setInterval(async () => {
|
|
130
|
+
console.clear();
|
|
131
|
+
printHeader();
|
|
132
|
+
console.log(chalk.cyan("Loop mode enabled. Press Ctrl+C to exit."));
|
|
133
|
+
console.log(
|
|
134
|
+
chalk.gray(`Last refresh: ${new Date().toLocaleTimeString()}\n`),
|
|
135
|
+
);
|
|
136
|
+
await fetchAndDisplayDevices(ctx);
|
|
137
|
+
}, REFRESH_INTERVAL);
|
|
138
|
+
|
|
139
|
+
// Keep the process running
|
|
140
|
+
await new Promise(() => {
|
|
141
|
+
// This promise never resolves - we wait for SIGINT
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
clearInterval(interval);
|
|
145
|
+
}
|
package/src/cli/commands/help.ts
CHANGED
|
@@ -78,6 +78,26 @@ ${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`,
|
|
81
101
|
};
|
|
82
102
|
|
|
83
103
|
export async function showHelp(command?: string): Promise<void> {
|
|
@@ -120,6 +140,9 @@ export async function showHelp(command?: string): Promise<void> {
|
|
|
120
140
|
chalk.green("device <id>") +
|
|
121
141
|
" Connect to a specific device\n" +
|
|
122
142
|
" " +
|
|
143
|
+
chalk.green("scan") +
|
|
144
|
+
" Scan local network for devices\n" +
|
|
145
|
+
" " +
|
|
123
146
|
chalk.green("transfer <file>") +
|
|
124
147
|
" Transfer a file to a device\n\n" +
|
|
125
148
|
chalk.bold("General:\n") +
|
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { debugLog, parseArgs, type CommandContext } from "../utils/context.js";
|
|
3
3
|
import { printHeader } from "../utils/ui.js";
|
|
4
|
+
import { checkForUpdates } from "../utils/update-checker.js";
|
|
4
5
|
|
|
5
6
|
export async function run() {
|
|
6
7
|
const args = process.argv.slice(2);
|
|
@@ -8,6 +9,9 @@ export async function run() {
|
|
|
8
9
|
|
|
9
10
|
const ctx: CommandContext = { debug: flags.debug };
|
|
10
11
|
|
|
12
|
+
// Check for updates (non-blocking)
|
|
13
|
+
checkForUpdates();
|
|
14
|
+
|
|
11
15
|
// Debug mode: log parsed arguments
|
|
12
16
|
debugLog(ctx, "Parsed arguments:", { command, flags, commandArgs });
|
|
13
17
|
|
|
@@ -47,7 +51,7 @@ export async function run() {
|
|
|
47
51
|
case "devices":
|
|
48
52
|
{
|
|
49
53
|
const { listDevices } = await import("./commands/devices.js");
|
|
50
|
-
await listDevices(ctx);
|
|
54
|
+
await listDevices(commandArgs, ctx);
|
|
51
55
|
}
|
|
52
56
|
break;
|
|
53
57
|
case "device":
|
|
@@ -62,6 +66,12 @@ export async function run() {
|
|
|
62
66
|
await transferFile(commandArgs[0], ctx);
|
|
63
67
|
}
|
|
64
68
|
break;
|
|
69
|
+
case "scan":
|
|
70
|
+
{
|
|
71
|
+
const { scanLocal } = await import("./commands/scan.js");
|
|
72
|
+
await scanLocal(commandArgs, ctx);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
65
75
|
case "help":
|
|
66
76
|
{
|
|
67
77
|
const { showHelp } = await import("./commands/help.js");
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
export const networkScanner = new NetworkScanner();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a newer version of the CLI is available on npm
|
|
11
|
+
*/
|
|
12
|
+
export async function checkForUpdates(): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
// Get current version from package.json
|
|
15
|
+
const packagePath = join(__dirname, "../../package.json");
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
17
|
+
const currentVersion = packageJson.version;
|
|
18
|
+
const packageName = packageJson.name;
|
|
19
|
+
|
|
20
|
+
// Fetch latest version from npm registry
|
|
21
|
+
const response = await fetch(
|
|
22
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
23
|
+
{
|
|
24
|
+
headers: { Accept: "application/json" },
|
|
25
|
+
signal: AbortSignal.timeout(3000), // 3 second timeout
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return; // Silently fail if npm check fails
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = (await response.json()) as { version: string };
|
|
34
|
+
const latestVersion = data.version;
|
|
35
|
+
|
|
36
|
+
// Compare versions
|
|
37
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
38
|
+
const isNewer = compareVersions(latestVersion, currentVersion) > 0;
|
|
39
|
+
|
|
40
|
+
if (isNewer) {
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.yellow.bold("╭─────────────────────────────────────────╮"),
|
|
44
|
+
);
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.yellow.bold("│") +
|
|
47
|
+
chalk.yellow(" Update available! ") +
|
|
48
|
+
chalk.gray(`${currentVersion}`) +
|
|
49
|
+
chalk.yellow(" → ") +
|
|
50
|
+
chalk.green.bold(`${latestVersion}`) +
|
|
51
|
+
chalk.yellow.bold(" │"),
|
|
52
|
+
);
|
|
53
|
+
console.log(
|
|
54
|
+
chalk.yellow.bold("│") +
|
|
55
|
+
chalk.cyan(` Run: npm i -g ${packageName}`) +
|
|
56
|
+
" ".repeat(7) +
|
|
57
|
+
chalk.yellow.bold("│"),
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
chalk.yellow.bold("╰─────────────────────────────────────────╯"),
|
|
61
|
+
);
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Silently fail - update check is not critical
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compare two semantic versions
|
|
72
|
+
* Returns: positive if a > b, negative if a < b, 0 if equal
|
|
73
|
+
*/
|
|
74
|
+
function compareVersions(a: string, b: string): number {
|
|
75
|
+
const partsA = a.split(".").map(Number);
|
|
76
|
+
const partsB = b.split(".").map(Number);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
const partA = partsA[i] || 0;
|
|
80
|
+
const partB = partsB[i] || 0;
|
|
81
|
+
if (partA !== partB) {
|
|
82
|
+
return partA - partB;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|