@involvex/syncstuff-cli 0.0.6 → 0.0.9

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.
@@ -1,146 +1,82 @@
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
-
1
+ import {
2
+ SYNCSTUFF_PROTOCOL,
3
+ SYNCSTUFF_SERVICE_TYPE,
4
+ type LocalDevice,
5
+ type ServiceTxtRecord,
6
+ } from "@syncstuff/network-types";
7
+ import { Bonjour, Browser, Service } from "bonjour-service";
8
+ import { createSocket } from "dgram";
9
+ import { createRequire } from "module";
10
+ import { v4 as uuidv4 } from "uuid";
11
+ import { readConfig, writeConfig } from "./config.js";
12
+
13
+ export type { LocalDevice };
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const packageJson = require("../../../package.json");
15
17
  /**
16
- * Network scanner for discovering SyncStuff devices on local network
17
- * Uses UDP multicast to find devices
18
+ * Network scanner for discovering SyncStuff devices on the local network using mDNS.
18
19
  */
19
20
  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[] = [];
21
+ private bonjour: Bonjour;
22
+ private browser: Browser | null = null;
23
+ private ad: Service | null = null;
28
24
 
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;
25
+ constructor() {
26
+ this.bonjour = new Bonjour();
41
27
  }
42
28
 
43
29
  /**
44
- * Scan the local network for SyncStuff devices
45
- * Uses UDP broadcast to discover devices
30
+ * Scan the local network for SyncStuff devices using mDNS.
46
31
  */
47
- async scan(timeout = 10000): Promise<LocalDevice[]> {
32
+ async scan(timeout = 5000): Promise<LocalDevice[]> {
48
33
  return new Promise(resolve => {
34
+ if (this.browser) {
35
+ this.browser.stop();
36
+ }
49
37
  const devices: LocalDevice[] = [];
50
38
  const seenIds = new Set<string>();
51
39
 
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
- );
40
+ this.browser = this.bonjour.find({
41
+ type: SYNCSTUFF_SERVICE_TYPE,
42
+ protocol: SYNCSTUFF_PROTOCOL,
43
+ });
44
+
45
+ this.browser.on("up", (service: Service) => {
46
+ const txt = (service.txt || {}) as unknown as ServiceTxtRecord;
47
+ const deviceId = txt.deviceId;
48
+
49
+ if (deviceId && !seenIds.has(deviceId)) {
50
+ const addresses = service.addresses || [];
51
+ const ip = addresses.find(addr => addr.includes("."));
52
+ if (ip) {
53
+ seenIds.add(deviceId);
54
+ devices.push({
55
+ id: deviceId,
56
+ name: txt.deviceName || service.name,
57
+ platform: txt.platform || "unknown",
58
+ ip,
59
+ port: service.port,
60
+ version: txt.version || "1.0.0",
61
+ });
107
62
  }
63
+ }
64
+ });
108
65
 
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);
66
+ setTimeout(() => {
67
+ if (this.browser) {
68
+ this.browser.stop();
69
+ this.browser = null;
70
+ }
126
71
  resolve(devices);
127
- }
72
+ }, timeout);
128
73
  });
129
74
  }
130
75
 
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
76
  /**
143
- * Send a message to a specific device
77
+ * Send a message to a specific device.
78
+ * Note: This still uses a direct UDP socket, which is fine for direct communication
79
+ * once a device's IP and port are known.
144
80
  */
145
81
  async sendTo(ip: string, port: number, message: object): Promise<void> {
146
82
  return new Promise((resolve, reject) => {
@@ -157,6 +93,70 @@ class NetworkScanner {
157
93
  });
158
94
  });
159
95
  }
96
+
97
+ /**
98
+ * Start advertising this device on the local network using mDNS.
99
+ */
100
+ startAdvertising(deviceName: string, port: number, platform = "cli") {
101
+ if (this.ad) {
102
+ this.ad.stop?.(() => {
103
+ this.ad = null;
104
+ this.publish(deviceName, port, platform);
105
+ });
106
+ } else {
107
+ this.publish(deviceName, port, platform);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Stop advertising this device.
113
+ */
114
+ stopAdvertising() {
115
+ if (this.ad) {
116
+ this.ad.stop?.(() => {
117
+ this.ad = null;
118
+ console.log("Stopped advertising.");
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Unpublish all services and destroy the bonjour instance.
125
+ */
126
+ destroy() {
127
+ this.bonjour.unpublishAll(() => {
128
+ this.bonjour.destroy();
129
+ });
130
+ }
131
+
132
+ private publish(deviceName: string, port: number, platform: string) {
133
+ const config = readConfig();
134
+ if (!config.deviceId) {
135
+ config.deviceId = uuidv4();
136
+ writeConfig(config);
137
+ }
138
+ const deviceId = config.deviceId;
139
+ const version = packageJson.version;
140
+
141
+ this.ad = this.bonjour.publish({
142
+ name: `${deviceName}-${deviceId.substring(0, 6)}`,
143
+ type: SYNCSTUFF_SERVICE_TYPE,
144
+ port,
145
+ protocol: SYNCSTUFF_PROTOCOL,
146
+ txt: {
147
+ deviceId,
148
+ deviceName,
149
+ platform,
150
+ version,
151
+ },
152
+ });
153
+ console.log(`Advertising '${deviceName}' on the network...`);
154
+ }
160
155
  }
161
156
 
162
157
  export const networkScanner = new NetworkScanner();
158
+
159
+ // Graceful shutdown
160
+ process.on("exit", () => networkScanner.destroy());
161
+ process.on("SIGINT", () => process.exit());
162
+ process.on("SIGTERM", () => process.exit());
package/src/utils/ui.ts CHANGED
@@ -55,7 +55,7 @@ export function createTable(data: string[][], headers?: string[]): string {
55
55
  });
56
56
  }
57
57
 
58
- export function animateText(text: string, delay: number = 50): Promise<void> {
58
+ export function animateText(text: string, delay = 50): Promise<void> {
59
59
  return new Promise(resolve => {
60
60
  let index = 0;
61
61
  const interval = setInterval(() => {
package/.eslintignore DELETED
@@ -1 +0,0 @@
1
- eslint.config.ts