@involvex/syncstuff-cli 0.0.6 → 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 CHANGED
@@ -9807,7 +9807,21 @@ ${source_default.bold("Description:")}
9807
9807
  ${source_default.bold("Examples:")}
9808
9808
  syncstuff scan Scan for 10 seconds
9809
9809
  syncstuff scan --timeout 30 Scan for 30 seconds
9810
- syncstuff scan --watch Continuously monitor for devices`
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`
9811
9825
  };
9812
9826
  });
9813
9827
 
@@ -31614,6 +31628,33 @@ class NetworkScanner {
31614
31628
  });
31615
31629
  });
31616
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
+ }
31617
31658
  }
31618
31659
  var SYNCSTUFF_PORT = 5353, networkScanner;
31619
31660
  var init_network = __esm(() => {
@@ -31734,6 +31775,101 @@ var init_scan = __esm(() => {
31734
31775
  init_ui();
31735
31776
  });
31736
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
+
31737
31873
  // src/core.ts
31738
31874
  var DebugMode;
31739
31875
  var init_core = __esm(() => {
@@ -31899,6 +32035,12 @@ async function run() {
31899
32035
  await scanLocal2(commandArgs, ctx);
31900
32036
  }
31901
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;
31902
32044
  case "help":
31903
32045
  {
31904
32046
  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.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "homepage": "https://syncstuff-web.involvex.workers.dev/",
6
6
  "sponsor": {
@@ -98,6 +98,21 @@ ${chalk.bold("Examples:")}
98
98
  syncstuff scan Scan for 10 seconds
99
99
  syncstuff scan --timeout 30 Scan for 30 seconds
100
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`,
101
116
  };
102
117
 
103
118
  export async function showHelp(command?: string): Promise<void> {
@@ -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
+ }
package/src/cli/index.ts CHANGED
@@ -72,6 +72,12 @@ export async function run() {
72
72
  await scanLocal(commandArgs, ctx);
73
73
  }
74
74
  break;
75
+ case "listen":
76
+ {
77
+ const { listen } = await import("./commands/listen.js");
78
+ await listen(commandArgs, ctx);
79
+ }
80
+ break;
75
81
  case "help":
76
82
  {
77
83
  const { showHelp } = await import("./commands/help.js");
@@ -157,6 +157,51 @@ class NetworkScanner {
157
157
  });
158
158
  });
159
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
+ }
160
205
  }
161
206
 
162
207
  export const networkScanner = new NetworkScanner();