@involvex/syncstuff-cli 0.0.5 → 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 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
 
@@ -31496,6 +31516,224 @@ var init_transfer = __esm(() => {
31496
31516
  init_ui();
31497
31517
  });
31498
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
+
31499
31737
  // src/core.ts
31500
31738
  var DebugMode;
31501
31739
  var init_core = __esm(() => {
@@ -31655,6 +31893,12 @@ async function run() {
31655
31893
  await transferFile2(commandArgs[0], ctx);
31656
31894
  }
31657
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;
31658
31902
  case "help":
31659
31903
  {
31660
31904
  const { showHelp: showHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/syncstuff-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "homepage": "https://syncstuff-web.involvex.workers.dev/",
6
6
  "sponsor": {
@@ -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
@@ -66,6 +66,12 @@ 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;
69
75
  case "help":
70
76
  {
71
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();