@involvex/syncstuff-cli 0.0.7 → 0.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/syncstuff-cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "homepage": "https://syncstuff-web.involvex.workers.dev/",
6
6
  "sponsor": {
@@ -29,7 +29,10 @@
29
29
  "inquirer": "13.1.0",
30
30
  "ora": "9.0.0",
31
31
  "boxen": "8.0.1",
32
- "table": "6.9.0"
32
+ "table": "6.9.0",
33
+ "bonjour-service": "^1.1.1",
34
+ "@syncstuff/network-types": "workspace:*",
35
+ "uuid": "^9.0.1"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@eslint/css": "0.14.1",
@@ -3,7 +3,6 @@ import { DebugMode } from "../../core";
3
3
  export const showDebug = () => {
4
4
  if (!DebugMode.enabled) {
5
5
  return;
6
- } else {
7
- console.log("Debug mode enabled");
8
6
  }
7
+ console.log("Debug mode enabled");
9
8
  };
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import inquirer from "inquirer";
3
3
  import { apiClient, type Device } from "../../utils/api-client.js";
4
- import { debugLog, type CommandContext } from "../../utils/context.js";
4
+ import { type CommandContext, debugLog } from "../../utils/context.js";
5
5
  import {
6
6
  createSpinner,
7
7
  createTable,
@@ -77,7 +77,7 @@ async function listAvailableDevices(ctx: CommandContext): Promise<Device[]> {
77
77
  }
78
78
 
79
79
  const tableData = response.data.map(device => [
80
- device.id.substring(0, 8) + "...",
80
+ `${device.id.substring(0, 8)}...`,
81
81
  device.name,
82
82
  device.type,
83
83
  device.platform,
@@ -100,19 +100,18 @@ async function listAvailableDevices(ctx: CommandContext): Promise<Device[]> {
100
100
  printSeparator();
101
101
 
102
102
  return response.data;
103
+ }
104
+ spinner.fail("Failed to fetch devices");
105
+ if (
106
+ response.error?.includes("404") ||
107
+ response.error?.includes("Not found")
108
+ ) {
109
+ info("Devices endpoint not yet implemented in API");
110
+ info("This feature will be available soon!");
103
111
  } else {
104
- spinner.fail("Failed to fetch devices");
105
- if (
106
- response.error?.includes("404") ||
107
- response.error?.includes("Not found")
108
- ) {
109
- info("Devices endpoint not yet implemented in API");
110
- info("This feature will be available soon!");
111
- } else {
112
- error(response.error || "Unknown error");
113
- }
114
- return [];
112
+ error(response.error || "Unknown error");
115
113
  }
114
+ return [];
116
115
  } catch (err) {
117
116
  spinner.fail("Error fetching devices");
118
117
  error(`Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { apiClient } from "../../utils/api-client.js";
3
- import { debugLog, type CommandContext } from "../../utils/context.js";
3
+ import { type CommandContext, debugLog } from "../../utils/context.js";
4
4
  import {
5
5
  createSpinner,
6
6
  createTable,
@@ -60,7 +60,7 @@ async function fetchAndDisplayDevices(ctx: CommandContext): Promise<boolean> {
60
60
  }
61
61
 
62
62
  const tableData = response.data.map(device => [
63
- device.id.substring(0, 8) + "...",
63
+ `${device.id.substring(0, 8)}...`,
64
64
  device.name,
65
65
  device.type,
66
66
  device.platform,
@@ -82,19 +82,18 @@ async function fetchAndDisplayDevices(ctx: CommandContext): Promise<boolean> {
82
82
  success(`Found ${response.data.length} device(s)`);
83
83
  printSeparator();
84
84
  return true;
85
+ }
86
+ spinner.fail("Failed to fetch devices");
87
+ if (
88
+ response.error?.includes("404") ||
89
+ response.error?.includes("Not found")
90
+ ) {
91
+ info("Devices endpoint not yet implemented in API");
92
+ info("This feature will be available soon!");
85
93
  } else {
86
- spinner.fail("Failed to fetch devices");
87
- if (
88
- response.error?.includes("404") ||
89
- response.error?.includes("Not found")
90
- ) {
91
- info("Devices endpoint not yet implemented in API");
92
- info("This feature will be available soon!");
93
- } else {
94
- error(response.error || "Unknown error");
95
- }
96
- return false;
94
+ error(response.error || "Unknown error");
97
95
  }
96
+ return false;
98
97
  } catch (err) {
99
98
  spinner.fail("Error fetching devices");
100
99
  error(`Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -1,6 +1,6 @@
1
1
  import inquirer from "inquirer";
2
2
  import { apiClient } from "../../utils/api-client.js";
3
- import { debugLog, type CommandContext } from "../../utils/context.js";
3
+ import { type CommandContext, debugLog } from "../../utils/context.js";
4
4
  import {
5
5
  createSpinner,
6
6
  error,
@@ -1,5 +1,5 @@
1
1
  import { apiClient } from "../../utils/api-client.js";
2
- import { debugLog, type CommandContext } from "../../utils/context.js";
2
+ import { type CommandContext, debugLog } from "../../utils/context.js";
3
3
  import { error, printHeader, printSeparator, success } from "../../utils/ui.js";
4
4
 
5
5
  export async function logout(ctx: CommandContext): Promise<void> {
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { debugLog, type CommandContext } from "../../utils/context.js";
3
- import { networkScanner, type LocalDevice } from "../../utils/network.js";
2
+ import { type CommandContext, debugLog } from "../../utils/context.js";
3
+ import { type LocalDevice, networkScanner } from "../../utils/network.js";
4
4
  import {
5
5
  createSpinner,
6
6
  createTable,
@@ -21,7 +21,7 @@ export async function scanLocal(
21
21
  ctx: CommandContext,
22
22
  ): Promise<void> {
23
23
  const timeout = args.includes("--timeout")
24
- ? parseInt(args[args.indexOf("--timeout") + 1] || "10", 10)
24
+ ? Number.parseInt(args[args.indexOf("--timeout") + 1] || "10", 10)
25
25
  : 10;
26
26
  const continuous = args.includes("--watch") || args.includes("-w");
27
27
 
@@ -135,7 +135,7 @@ async function continuousScan(
135
135
 
136
136
  function displayDevices(devices: LocalDevice[]): void {
137
137
  const tableData = devices.map(device => [
138
- device.id.substring(0, 8) + "...",
138
+ `${device.id.substring(0, 8)}...`,
139
139
  device.name,
140
140
  device.platform,
141
141
  `${device.ip}:${device.port}`,
@@ -3,7 +3,7 @@ import { existsSync, statSync } from "fs";
3
3
  import inquirer from "inquirer";
4
4
  import { basename, resolve } from "path";
5
5
  import { apiClient } from "../../utils/api-client.js";
6
- import { debugLog, type CommandContext } from "../../utils/context.js";
6
+ import { type CommandContext, debugLog } from "../../utils/context.js";
7
7
  import {
8
8
  createSpinner,
9
9
  error,
@@ -18,8 +18,8 @@ export function showversion() {
18
18
 
19
19
  if (DebugMode.enabled === true) {
20
20
  console.log(chalk.yellow("Debug mode enabled"));
21
- console.log("Package path: " + packagePath);
22
- console.log("Version: " + version);
21
+ console.log(`Package path: ${packagePath}`);
22
+ console.log(`Version: ${version}`);
23
23
  }
24
24
 
25
25
  const versionBox = createBox(
@@ -27,7 +27,7 @@ export function showversion() {
27
27
  chalk.bold("Version:") +
28
28
  ` ${chalk.green(version)}\n` +
29
29
  chalk.bold("Package:") +
30
- ` @involvex/syncstuff-cli`,
30
+ " @involvex/syncstuff-cli",
31
31
  {
32
32
  title: "Version Information",
33
33
  titleAlignment: "center",
@@ -40,7 +40,7 @@ export function showversion() {
40
40
  if (DebugMode.enabled === true) {
41
41
  const packagePath = join(__dirname, "../../../package.json");
42
42
  console.log(chalk.yellow("Debug mode enabled"));
43
- console.log("Packagepath: " + packagePath);
43
+ console.log(`Packagepath: ${packagePath}`);
44
44
  console.log(chalk.yellow("Version: 0.0.1 (unable to read package.json)"));
45
45
  }
46
46
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { apiClient } from "../../utils/api-client.js";
3
- import { debugLog, type CommandContext } from "../../utils/context.js";
3
+ import { type CommandContext, debugLog } from "../../utils/context.js";
4
4
  import {
5
5
  createBox,
6
6
  createSpinner,
package/src/cli/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { debugLog, parseArgs, type CommandContext } from "../utils/context.js";
2
+ import { type CommandContext, debugLog, parseArgs } from "../utils/context.js";
3
3
  import { printHeader } from "../utils/ui.js";
4
4
  import { checkForUpdates } from "../utils/update-checker.js";
5
5
 
@@ -12,6 +12,7 @@ export interface Config {
12
12
  role: string;
13
13
  } | null;
14
14
  apiUrl?: string;
15
+ deviceId?: string;
15
16
  }
16
17
 
17
18
  const CONFIG_DIR = join(homedir(), ".syncstuff");
@@ -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) => {
@@ -159,49 +95,68 @@ class NetworkScanner {
159
95
  }
160
96
 
161
97
  /**
162
- * Start advertising this device on the local network
98
+ * Start advertising this device on the local network using mDNS.
163
99
  */
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(),
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);
184
105
  });
106
+ } else {
107
+ this.publish(deviceName, port, platform);
108
+ }
109
+ }
185
110
 
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
- };
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
+ }
198
122
 
199
- // Advertise immediately
200
- advertise();
123
+ /**
124
+ * Unpublish all services and destroy the bonjour instance.
125
+ */
126
+ destroy() {
127
+ this.bonjour.unpublishAll(() => {
128
+ this.bonjour.destroy();
129
+ });
130
+ }
201
131
 
202
- // Then every 3 seconds
203
- return setInterval(advertise, 3000);
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...`);
204
154
  }
205
155
  }
206
156
 
207
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