instbyte 1.11.0 → 1.12.0

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/README.md CHANGED
@@ -189,6 +189,8 @@ The difference between *a tool you use* and *a tool you own.*
189
189
 
190
190
  **Security hardened** — rate limiting on all write endpoints, magic number file validation, filename sanitisation, and forced download for executable file types.
191
191
 
192
+ **CLI companion** — `instbyte send`, `instbyte watch`, and `instbyte status` let you push files, pipe command output, sync clipboard, and check server health without opening a browser. Auto-discovers the running server from your working directory.
193
+
192
194
  ---
193
195
 
194
196
  ## Broadcasting
@@ -220,7 +222,7 @@ For HTTPS setup and advanced network configuration, see the [Deployment Guide](d
220
222
  git clone https://github.com/mohitgauniyal/instbyte
221
223
  cd instbyte
222
224
  npm install
223
- node server/server.js
225
+ node bin/instbyte.js
224
226
  ```
225
227
 
226
228
  ---
@@ -237,9 +239,19 @@ node server/server.js
237
239
 
238
240
  ---
239
241
 
240
- ## Terminal Usage
242
+ ## CLI & Terminal Usage
243
+
244
+ Send files, pipe command output, watch channels, and check server status from your terminal — no browser needed.
245
+
246
+ ```bash
247
+ instbyte send ./build.zip --channel assets
248
+ instbyte send "http://staging.myapp.com"
249
+ git log --oneline -20 | instbyte send
250
+ instbyte watch --channel projects
251
+ instbyte status
252
+ ```
241
253
 
242
- Push content from your terminal using curl — no browser needed. See [Terminal Usage Guide](docs/terminal-usage.md).
254
+ See the [Terminal Usage Guide](docs/terminal-usage.md) for the full CLI reference, CI/CD setup, and raw curl fallback.
243
255
 
244
256
  ---
245
257
 
@@ -253,7 +265,7 @@ Instbyte follows [Semantic Versioning](https://semver.org). See [Releases](https
253
265
 
254
266
  Instbyte is intentionally lightweight and LAN-first. If you want to extend it — CLI tools, themes, integrations — open an issue or submit a pull request.
255
267
 
256
- The codebase has a full test suite (195 tests across unit and integration). Run `npm test` before submitting anything. Issues tagged **good first issue** are a good starting point.
268
+ The codebase has a full test suite (214 tests across unit and integration). Run `npm test` before submitting anything. Issues tagged **good first issue** are a good starting point.
257
269
 
258
270
  ---
259
271
 
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const RUNTIME_FILE = path.join("instbyte-data", ".runtime.json");
8
+
9
+ // Walk up from dir looking for instbyte-data/.runtime.json.
10
+ // Stops at the user's home directory.
11
+ function findRuntimeConfig(dir) {
12
+ const candidate = path.join(dir, RUNTIME_FILE);
13
+ if (fs.existsSync(candidate)) {
14
+ try { return JSON.parse(fs.readFileSync(candidate, "utf-8")); }
15
+ catch { return null; }
16
+ }
17
+
18
+ const parent = path.dirname(dir);
19
+ if (parent === dir || dir === os.homedir()) return null; // reached root or home
20
+
21
+ return findRuntimeConfig(parent);
22
+ }
23
+
24
+ // Returns { url, passphrase } for CLI commands.
25
+ // Priority: explicit flags → env vars → .runtime.json walk → localhost fallback
26
+ function resolveServer(flags = {}) {
27
+ if (flags.server) {
28
+ return {
29
+ url: flags.server.replace(/\/$/, ""),
30
+ passphrase: flags.passphrase || process.env.INSTBYTE_PASS || ""
31
+ };
32
+ }
33
+
34
+ if (process.env.INSTBYTE_URL) {
35
+ return {
36
+ url: process.env.INSTBYTE_URL.replace(/\/$/, ""),
37
+ passphrase: flags.passphrase || process.env.INSTBYTE_PASS || ""
38
+ };
39
+ }
40
+
41
+ const runtime = findRuntimeConfig(process.cwd());
42
+ if (runtime && runtime.url) {
43
+ return {
44
+ url: runtime.url,
45
+ passphrase: flags.passphrase || runtime.passphrase || ""
46
+ };
47
+ }
48
+
49
+ return {
50
+ url: "http://localhost:3000",
51
+ passphrase: flags.passphrase || ""
52
+ };
53
+ }
54
+
55
+ module.exports = { resolveServer, findRuntimeConfig };
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const { resolveServer } = require("./resolve");
8
+
9
+ // argv after 'send'
10
+ const argv = process.argv.slice(3);
11
+
12
+ function parseArgs(argv) {
13
+ const flags = {};
14
+ let positional = null;
15
+
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const arg = argv[i];
18
+ if (arg.startsWith("--")) {
19
+ const key = arg.slice(2);
20
+ const next = argv[i + 1];
21
+ if (next && !next.startsWith("--")) {
22
+ flags[key] = next;
23
+ i++;
24
+ } else {
25
+ flags[key] = true;
26
+ }
27
+ } else if (positional === null) {
28
+ positional = arg;
29
+ }
30
+ }
31
+
32
+ return { flags, positional };
33
+ }
34
+
35
+ // Null byte in the first 512 bytes → binary
36
+ function isBinary(buf) {
37
+ const limit = Math.min(buf.length, 512);
38
+ for (let i = 0; i < limit; i++) {
39
+ if (buf[i] === 0) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ // fs.readFileSync(0) reads fd 0 (stdin) synchronously — more portable than
45
+ // event-based stream reading, and works correctly in Git Bash / MINGW64.
46
+ function readStdin() {
47
+ return fs.readFileSync(0);
48
+ }
49
+
50
+ function fail(msg, hint) {
51
+ console.error(`✗ ${msg}`);
52
+ if (hint) console.error(` ${hint}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ async function run() {
57
+ const { flags, positional } = parseArgs(argv);
58
+
59
+ if (flags.help) {
60
+ console.log("Usage: instbyte send [<file|text>] [--channel <name>] [--server <url>] [--passphrase <pass>] [--uploader <name>] [--quiet]");
61
+ process.exit(0);
62
+ }
63
+
64
+ const { url, passphrase } = resolveServer(flags);
65
+ const channel = flags.channel || "general";
66
+ const uploader = flags.uploader || os.userInfo().username;
67
+ const quiet = !!flags.quiet;
68
+
69
+ const authHeader = passphrase ? { "X-Passphrase": passphrase } : {};
70
+
71
+ // ── FILE ──────────────────────────────────────────────────────────────────
72
+ if (positional && fs.existsSync(positional)) {
73
+ const filePath = path.resolve(positional);
74
+ const filename = path.basename(filePath);
75
+
76
+ const form = new FormData();
77
+ form.append("file", new Blob([fs.readFileSync(filePath)]), filename);
78
+ form.append("channel", channel);
79
+ form.append("uploader", uploader);
80
+
81
+ let res;
82
+ try {
83
+ res = await fetch(`${url}/upload`, { method: "POST", headers: authHeader, body: form });
84
+ } catch {
85
+ fail(
86
+ "Connection failed — is the server running?",
87
+ `Start with: npx instbyte\nOr specify: instbyte send ${positional} --server http://192.168.x.x:3000`
88
+ );
89
+ }
90
+
91
+ if (res.status === 401) fail("Unauthorized — wrong passphrase or auth required");
92
+ if (!res.ok) fail(`Server error: ${res.status}`);
93
+
94
+ if (!quiet) console.log(`✓ Sent to ${channel} — ${url}`);
95
+ process.exit(0);
96
+ }
97
+
98
+ // ── TEXT (positional arg that is not a file path) ─────────────────────────
99
+ if (positional) {
100
+ let res;
101
+ try {
102
+ res = await fetch(`${url}/push`, {
103
+ method: "POST",
104
+ headers: { ...authHeader, "Content-Type": "text/plain", "X-Channel": channel, "X-Uploader": uploader },
105
+ body: positional
106
+ });
107
+ } catch {
108
+ fail(
109
+ "Connection failed — is the server running?",
110
+ `Start with: npx instbyte\nOr specify: instbyte send "${positional}" --server http://192.168.x.x:3000`
111
+ );
112
+ }
113
+
114
+ if (res.status === 401) fail("Unauthorized — wrong passphrase or auth required");
115
+ if (!res.ok) fail(`Server error: ${res.status}`);
116
+
117
+ if (!quiet) console.log(`✓ Sent to ${channel} — ${url}`);
118
+ process.exit(0);
119
+ }
120
+
121
+ // ── STDIN ─────────────────────────────────────────────────────────────────
122
+ if (!process.stdin.isTTY) {
123
+ let buf;
124
+ try { buf = readStdin(); }
125
+ catch { fail("Failed to read stdin"); }
126
+
127
+ if (isBinary(buf)) {
128
+ fail("Binary stdin detected — use `instbyte send ./file` to upload a file");
129
+ }
130
+
131
+ const content = buf.toString("utf-8");
132
+ if (!content.trim()) fail("Empty input — nothing to send");
133
+
134
+ let res;
135
+ try {
136
+ res = await fetch(`${url}/push`, {
137
+ method: "POST",
138
+ headers: { ...authHeader, "Content-Type": "text/plain", "X-Channel": channel, "X-Uploader": uploader },
139
+ body: content
140
+ });
141
+ } catch {
142
+ fail(
143
+ "Connection failed — is the server running?",
144
+ "Start with: npx instbyte\nOr specify: instbyte send --server http://192.168.x.x:3000"
145
+ );
146
+ }
147
+
148
+ if (res.status === 401) fail("Unauthorized — wrong passphrase or auth required");
149
+ if (!res.ok) fail(`Server error: ${res.status}`);
150
+
151
+ if (!quiet) console.log(`✓ Sent to ${channel} — ${url}`);
152
+ process.exit(0);
153
+ }
154
+
155
+ // ── NO INPUT ──────────────────────────────────────────────────────────────
156
+ console.error("Usage: instbyte send <file|text>");
157
+ console.error(" cat file.log | instbyte send");
158
+ process.exit(1);
159
+ }
160
+
161
+ run();
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const { resolveServer } = require("./resolve");
4
+
5
+ const argv = process.argv.slice(3);
6
+
7
+ function parseArgs(argv) {
8
+ const flags = {};
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const arg = argv[i];
11
+ if (arg.startsWith("--")) {
12
+ const key = arg.slice(2);
13
+ const next = argv[i + 1];
14
+ if (next && !next.startsWith("--")) { flags[key] = next; i++; }
15
+ else flags[key] = true;
16
+ }
17
+ }
18
+ return flags;
19
+ }
20
+
21
+ function formatUptime(seconds) {
22
+ if (seconds < 60) return `${seconds}s`;
23
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
24
+ const h = Math.floor(seconds / 3600);
25
+ const m = Math.floor((seconds % 3600) / 60);
26
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
27
+ }
28
+
29
+ async function run() {
30
+ const flags = parseArgs(argv);
31
+
32
+ if (flags.help) {
33
+ console.log("Usage: instbyte status [--server <url>] [--passphrase <pass>]");
34
+ process.exit(0);
35
+ }
36
+
37
+ const { url, passphrase } = resolveServer(flags);
38
+ const headers = passphrase ? { "X-Passphrase": passphrase } : {};
39
+
40
+ let health, channels;
41
+
42
+ try {
43
+ const [hRes, cRes] = await Promise.all([
44
+ fetch(`${url}/health`, { headers }),
45
+ fetch(`${url}/channels`, { headers })
46
+ ]);
47
+
48
+ if (!hRes.ok) throw new Error(`${hRes.status}`);
49
+ health = await hRes.json();
50
+ channels = await cRes.json();
51
+ } catch {
52
+ console.error(`✗ No Instbyte server found at ${url}`);
53
+ console.error(` Start one with: npx instbyte`);
54
+ console.error(` Or specify: instbyte status --server http://192.168.x.x:3000`);
55
+ process.exit(1);
56
+ }
57
+
58
+ const channelNames = channels.map(c => c.name).join(", ");
59
+
60
+ console.log(`✓ Instbyte v${health.version} running at ${url}`);
61
+ console.log(` Uptime: ${formatUptime(health.uptime)}`);
62
+ console.log(` Connected: ${health.connected} user${health.connected !== 1 ? "s" : ""}`);
63
+ console.log(` Channels: ${channelNames}`);
64
+ if (health.hasAuth) console.log(` Auth: enabled`);
65
+ }
66
+
67
+ run();
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ const { spawn } = require("child_process");
4
+ const os = require("os");
5
+ const { resolveServer } = require("./resolve");
6
+
7
+ const argv = process.argv.slice(3);
8
+
9
+ function parseArgs(argv) {
10
+ const flags = {};
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const arg = argv[i];
13
+ if (arg.startsWith("--")) {
14
+ const key = arg.slice(2);
15
+ const next = argv[i + 1];
16
+ if (next && !next.startsWith("--")) { flags[key] = next; i++; }
17
+ else flags[key] = true;
18
+ }
19
+ }
20
+ return flags;
21
+ }
22
+
23
+ // Write text to the system clipboard.
24
+ // Falls back to printing the text if the clipboard tool is not found.
25
+ function copyToClipboard(text) {
26
+ let cmd, args;
27
+ if (process.platform === "darwin") {
28
+ cmd = "pbcopy"; args = [];
29
+ } else if (process.platform === "win32") {
30
+ cmd = "clip"; args = [];
31
+ } else {
32
+ cmd = "xclip"; args = ["-sel", "c"];
33
+ }
34
+
35
+ const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
36
+ proc.on("error", () => process.stdout.write(text + "\n")); // fallback
37
+ proc.stdin.write(text, "utf-8");
38
+ proc.stdin.end();
39
+ }
40
+
41
+ function truncate(str, len) {
42
+ return str.length > len ? str.slice(0, len) + "…" : str;
43
+ }
44
+
45
+ function run() {
46
+ const flags = parseArgs(argv);
47
+
48
+ if (flags.help) {
49
+ console.log("Usage: instbyte watch [--channel <name>] [--server <url>] [--passphrase <pass>] [--output]");
50
+ process.exit(0);
51
+ }
52
+
53
+ const { url } = resolveServer(flags);
54
+ const channel = flags.channel || "general";
55
+ const outputMode = !!flags.output;
56
+
57
+ // In --output mode, status messages go to stderr so stdout stays clean for piping
58
+ const status = outputMode
59
+ ? (msg) => process.stderr.write(msg + "\n")
60
+ : (msg) => process.stdout.write(msg + "\n");
61
+
62
+ const { io } = require("socket.io-client");
63
+
64
+ const socket = io(url, {
65
+ reconnection: true,
66
+ reconnectionDelay: 2000,
67
+ reconnectionDelayMax: 10000
68
+ });
69
+
70
+ let connected = false;
71
+ let connectErrShown = false;
72
+ let stopping = false;
73
+
74
+ socket.on("connect", () => {
75
+ socket.emit("join", os.userInfo().username);
76
+ status(connected
77
+ ? "Reconnected."
78
+ : `Watching ${channel} on ${url}... (Ctrl+C to stop)`
79
+ );
80
+ connected = true;
81
+ connectErrShown = false;
82
+ });
83
+
84
+ socket.on("disconnect", () => {
85
+ if (stopping) return;
86
+ connected = false;
87
+ status("Reconnecting...");
88
+ });
89
+
90
+ socket.on("connect_error", () => {
91
+ if (!connectErrShown) {
92
+ status(`✗ Cannot connect to ${url}`);
93
+ status(` Start a server with: npx instbyte`);
94
+ connectErrShown = true;
95
+ }
96
+ });
97
+
98
+ socket.on("new-item", (item) => {
99
+ if (item.channel !== channel) return;
100
+
101
+ if (item.type === "file") {
102
+ process.stdout.write(`📎 File: ${url}/uploads/${item.filename}\n`);
103
+ return;
104
+ }
105
+
106
+ if (item.type === "text") {
107
+ if (outputMode) {
108
+ process.stdout.write(item.content + "\n");
109
+ } else {
110
+ copyToClipboard(item.content);
111
+ process.stdout.write(`✓ Copied: "${truncate(item.content, 60)}"\n`);
112
+ }
113
+ }
114
+ });
115
+
116
+ process.on("SIGINT", () => {
117
+ stopping = true;
118
+ socket.disconnect();
119
+ process.stdout.write("\nStopped watching.\n");
120
+ process.exit(0);
121
+ });
122
+ }
123
+
124
+ run();
package/bin/instbyte.js CHANGED
@@ -3,35 +3,90 @@
3
3
  "use strict";
4
4
 
5
5
  const path = require("path");
6
- const fs = require("fs");
6
+ const fs = require("fs");
7
+
8
+ const args = process.argv.slice(2);
9
+ const subcommand = args[0];
10
+
11
+ const SUBCOMMANDS = ["send", "watch", "status"];
7
12
 
8
13
  // ========================
9
- // DATA DIRECTORY SETUP
14
+ // HELP
10
15
  // ========================
11
- // When run via npx or global install, we want data to live
12
- // in the user's current working directory, not inside the
13
- // npm cache or global node_modules.
14
- //
15
- // When run via Docker, INSTBYTE_DATA and INSTBYTE_UPLOADS may
16
- // already be set via environment variables — respect those and
17
- // don't override them.
18
-
19
- if (!process.env.INSTBYTE_DATA) {
20
- process.env.INSTBYTE_DATA = path.join(process.cwd(), "instbyte-data");
16
+ function showHelp() {
17
+ console.log(`
18
+ Usage:
19
+ instbyte Start the server
20
+ instbyte send <file|text> Send a file or text to a channel
21
+ instbyte send Read from stdin and send
22
+ instbyte watch Watch a channel, copy new text to clipboard
23
+ instbyte status Check if a server is running
24
+
25
+ Options (send / watch / status):
26
+ --server <url> Server URL (overrides auto-discovery)
27
+ --channel <name> Target channel (default: general)
28
+ --passphrase <pass> Passphrase if auth is enabled
29
+ --uploader <name> Display name for sent items (default: OS username)
30
+ --output watch: print to stdout instead of clipboard
31
+ --quiet send: suppress confirmation output
32
+
33
+ Examples:
34
+ instbyte send ./build.zip --channel assets
35
+ instbyte send "http://staging.myapp.com"
36
+ git log --oneline -20 | instbyte send
37
+ instbyte watch --channel projects
38
+ instbyte watch --output | grep error
39
+ instbyte status --server http://192.168.1.10:3000
40
+ `);
21
41
  }
22
42
 
23
- if (!process.env.INSTBYTE_UPLOADS) {
24
- process.env.INSTBYTE_UPLOADS = path.join(process.env.INSTBYTE_DATA, "uploads");
43
+ // ========================
44
+ // ROUTE
45
+ // ========================
46
+ if (args.includes("--help") || args.includes("-h")) {
47
+ if (!SUBCOMMANDS.includes(subcommand)) {
48
+ showHelp();
49
+ process.exit(0);
50
+ }
25
51
  }
26
52
 
27
- const dataDir = process.env.INSTBYTE_DATA;
28
- const uploadsDir = process.env.INSTBYTE_UPLOADS;
53
+ if (SUBCOMMANDS.includes(subcommand)) {
54
+ // Each command module is loaded only when needed
55
+ const mod = path.join(__dirname, "cli", subcommand + ".js");
56
+ if (!fs.existsSync(mod)) {
57
+ console.error(`${subcommand}: not implemented yet`);
58
+ process.exit(1);
59
+ }
60
+ require(mod);
29
61
 
30
- if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
31
- if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
62
+ } else if (subcommand && !subcommand.startsWith("-")) {
63
+ console.error(`Unknown command: ${subcommand}`);
64
+ showHelp();
65
+ process.exit(1);
66
+
67
+ } else {
68
+ // No subcommand (or only flags) — start the server
69
+ startServer();
70
+ }
32
71
 
33
72
  // ========================
34
- // BOOT
73
+ // SERVER START
35
74
  // ========================
36
- process.env.INSTBYTE_BOOT = '1';
37
- require("../server/server.js");
75
+ function startServer() {
76
+ if (!process.env.INSTBYTE_DATA) {
77
+ process.env.INSTBYTE_DATA = path.join(process.cwd(), "instbyte-data");
78
+ }
79
+
80
+ if (!process.env.INSTBYTE_UPLOADS) {
81
+ process.env.INSTBYTE_UPLOADS = path.join(process.env.INSTBYTE_DATA, "uploads");
82
+ }
83
+
84
+ const dataDir = process.env.INSTBYTE_DATA;
85
+ const uploadsDir = process.env.INSTBYTE_UPLOADS;
86
+
87
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
88
+ if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
89
+
90
+ process.env.INSTBYTE_BOOT = "1";
91
+ require("../server/server.js");
92
+ }
package/client/js/app.js CHANGED
@@ -425,6 +425,17 @@ async function togglePreview(id, filename) {
425
425
  }
426
426
  }
427
427
 
428
+ function safeCreateIcons(container) {
429
+ if (typeof lucide === "undefined") return;
430
+ requestAnimationFrame(() => {
431
+ if (container) {
432
+ lucide.createIcons({ nodes: [container] });
433
+ } else {
434
+ lucide.createIcons();
435
+ }
436
+ });
437
+ }
438
+
428
439
  function handleRowClick(el, type, value) {
429
440
  if (type === "text") {
430
441
  navigator.clipboard.writeText(value).then(() => {
@@ -720,11 +731,6 @@ function buildItemEl(i) {
720
731
  <div class="preview-panel" id="preview-${i.id}"></div>`;
721
732
 
722
733
  seenObserver.observe(div);
723
- if (typeof lucide !== "undefined") {
724
- lucide.createIcons({ nodes: [div] });
725
- } else {
726
- console.warn("Lucide not yet defined when building item", i.id);
727
- }
728
734
  return div;
729
735
  }
730
736
 
@@ -896,18 +902,18 @@ function editContent(id) {
896
902
  function render(data) {
897
903
  const el = document.getElementById("items");
898
904
  el.innerHTML = "";
899
-
900
905
  if (!data.length) {
901
906
  el.innerHTML = `<div class="empty-state">Nothing here yet — paste, type, or drop a file to share</div>`;
902
907
  return;
903
908
  }
904
-
905
909
  data.forEach(i => el.appendChild(buildItemEl(i)));
910
+ safeCreateIcons(el);
906
911
  }
907
912
 
908
913
  function renderMore(data) {
909
914
  const el = document.getElementById("items");
910
915
  data.forEach(i => el.appendChild(buildItemEl(i)));
916
+ safeCreateIcons(el);
911
917
  }
912
918
 
913
919
  function renderGrouped(data) {
@@ -929,12 +935,13 @@ function renderGrouped(data) {
929
935
  const section = document.createElement("div");
930
936
  section.style.marginTop = "20px";
931
937
  section.innerHTML = `
932
- <div style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:8px;">
933
- ${ch.toUpperCase()}
934
- </div>`;
938
+ <div style="font-size:13px;font-weight:600;color:#6b7280;margin-bottom:8px;">
939
+ ${ch.toUpperCase()}
940
+ </div>`;
935
941
  grouped[ch].forEach(i => section.appendChild(buildItemEl(i)));
936
942
  el.appendChild(section);
937
943
  });
944
+ safeCreateIcons(el); // once after all sections appended, not inside the loop
938
945
  }
939
946
 
940
947
  async function sendText() {
@@ -1111,7 +1118,7 @@ socket.on("new-item", item => {
1111
1118
  }
1112
1119
  }
1113
1120
 
1114
- if (typeof lucide !== "undefined") lucide.createIcons({ nodes: [el] });
1121
+ safeCreateIcons(el);
1115
1122
  if (item.uploader !== uploader) playChime();
1116
1123
 
1117
1124
  } else if (item.uploader !== uploader) {
@@ -1378,9 +1385,7 @@ async function uploadFiles(files, overrideChannel) {
1378
1385
 
1379
1386
  status.style.display = "none";
1380
1387
  fileInput.value = "";
1381
- // Delayed fallback socket event should have already updated the DOM by now,
1382
- // but this catches any case where the socket path didn't fire (e.g. network hiccup).
1383
- setTimeout(() => load(), 500);
1388
+ // socket new-item handles DOM update in real time no reload needed
1384
1389
  }
1385
1390
 
1386
1391
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "A self-hosted LAN sharing utility for fast, frictionless file, link, and snippet exchange across devices — no cloud required.",
5
5
  "main": "bin/instbyte.js",
6
6
  "bin": {
@@ -43,9 +43,9 @@
43
43
  "express-rate-limit": "^7.1.5",
44
44
  "helmet": "^8.1.0",
45
45
  "multer": "^2.0.2",
46
- "multicast-dns": "^7.2.5",
47
46
  "sharp": "^0.33.2",
48
47
  "socket.io": "^4.6.1",
48
+ "socket.io-client": "^4.6.1",
49
49
  "sqlite3": "^5.1.6"
50
50
  },
51
51
  "devDependencies": {
package/server/server.js CHANGED
@@ -19,8 +19,6 @@ const db = require("./db");
19
19
 
20
20
  const config = require("./config");
21
21
 
22
- let mdns = null;
23
- try { mdns = require('multicast-dns'); } catch (e) { }
24
22
 
25
23
  const UPLOADS_DIR = process.env.INSTBYTE_UPLOADS
26
24
  || path.join(__dirname, "../uploads");
@@ -857,7 +855,8 @@ app.get("/health", (req, res) => {
857
855
  res.json({
858
856
  status: "ok",
859
857
  uptime: Math.floor(process.uptime()),
860
- version: require("../package.json").version
858
+ version: require("../package.json").version,
859
+ connected: connectedUsers
861
860
  });
862
861
  });
863
862
 
@@ -1080,62 +1079,6 @@ function getLocalIP() {
1080
1079
  }
1081
1080
 
1082
1081
 
1083
- function getMdnsName() {
1084
- const appName = config.branding && config.branding.appName;
1085
- if (appName && appName.toLowerCase() !== 'instbyte') {
1086
- return appName.toLowerCase().replace(/\s+/g, '-') + '.local';
1087
- }
1088
- return 'instbyte.local';
1089
- }
1090
-
1091
- function getHostnameFallback() {
1092
- return os.hostname().toLowerCase() + '.local';
1093
- }
1094
-
1095
- function probeMdns(name) {
1096
- return new Promise((resolve) => {
1097
- if (!mdns) return resolve(false);
1098
- const m = mdns();
1099
- const timer = setTimeout(() => {
1100
- m.destroy();
1101
- resolve(false); // no response = name is free
1102
- }, 2000);
1103
-
1104
- m.on('response', (response) => {
1105
- const taken = response.answers.some(a =>
1106
- a.name === name && a.type === 'A'
1107
- );
1108
- if (taken) {
1109
- clearTimeout(timer);
1110
- m.destroy();
1111
- resolve(true); // name is taken
1112
- }
1113
- });
1114
-
1115
- m.query({ questions: [{ name, type: 'A' }] });
1116
- });
1117
- }
1118
-
1119
- function startMdnsAdvertise(name, ip) {
1120
- if (!mdns) return null;
1121
- try {
1122
- const m = mdns();
1123
- m.on('query', (query) => {
1124
- query.questions.forEach(q => {
1125
- if (q.name === name && q.type === 'A') {
1126
- m.respond({
1127
- answers: [{ name, type: 'A', ttl: 300, data: ip }]
1128
- });
1129
- }
1130
- });
1131
- });
1132
- return m;
1133
- } catch (e) {
1134
- return null;
1135
- }
1136
- }
1137
-
1138
-
1139
1082
  function listenOnFreePort(server, preferredPort) {
1140
1083
  return new Promise((resolve, reject) => {
1141
1084
  server.listen(preferredPort, () => resolve(server.address().port));
@@ -1150,12 +1093,28 @@ function listenOnFreePort(server, preferredPort) {
1150
1093
  });
1151
1094
  }
1152
1095
 
1096
+ function writeRuntimeConfig(ip, port) {
1097
+ const dataDir = process.env.INSTBYTE_DATA;
1098
+ if (!dataDir) return;
1099
+
1100
+ const runtimePath = path.join(dataDir, ".runtime.json");
1101
+ const payload = JSON.stringify({
1102
+ url: `http://${ip}:${port}`,
1103
+ passphrase: config.auth.passphrase
1104
+ }, null, 2);
1105
+
1106
+ try {
1107
+ fs.writeFileSync(runtimePath, payload, "utf-8");
1108
+ } catch (e) {
1109
+ console.warn("Warning: could not write CLI runtime config:", e.message);
1110
+ }
1111
+ }
1112
+
1153
1113
  const PREFERRED = parseInt(process.env.PORT) || config.server.port;
1154
1114
 
1155
1115
  const localIP = getLocalIP();
1156
1116
 
1157
1117
  let PORT;
1158
- let mdnsAdvertiser = null;
1159
1118
 
1160
1119
  if (process.env.INSTBYTE_BOOT === '1') {
1161
1120
  listenOnFreePort(server, PREFERRED).then(async p => {
@@ -1167,34 +1126,8 @@ if (process.env.INSTBYTE_BOOT === '1') {
1167
1126
  console.log(`(port ${PREFERRED} was busy, switched to ${PORT})`);
1168
1127
  }
1169
1128
 
1170
- // mDNS
1171
- if (mdns) {
1172
- try {
1173
- const preferredName = getMdnsName();
1174
- const isTaken = await probeMdns(preferredName);
1175
-
1176
- let mdnsName;
1177
- if (isTaken) {
1178
- mdnsName = getHostnameFallback();
1179
- if (os.platform() !== 'win32') {
1180
- console.log(`mDNS: http://${preferredName} already in use on this network`);
1181
- console.log(`mDNS: http://${mdnsName}`);
1182
- }
1183
- } else {
1184
- mdnsName = preferredName;
1185
- if (os.platform() !== 'win32') {
1186
- console.log(`mDNS: http://${mdnsName}`);
1187
- }
1188
- }
1189
-
1190
- mdnsAdvertiser = startMdnsAdvertise(mdnsName, localIP);
1191
-
1192
- } catch (e) {
1193
- console.log("mDNS: unavailable on this network — use IP or QR code to join");
1194
- }
1195
- }
1196
-
1197
1129
  console.log("");
1130
+ writeRuntimeConfig(localIP, PORT);
1198
1131
  scanOrphans();
1199
1132
  }).catch(err => {
1200
1133
  console.error("Failed to start server:", err.message);
@@ -1211,11 +1144,6 @@ module.exports = { app, server, sessions };
1211
1144
  function shutdown(signal) {
1212
1145
  console.log(`\n${signal} received — shutting down gracefully...`);
1213
1146
 
1214
- // destroy mDNS advertiser if running
1215
- if (typeof mdnsAdvertiser !== 'undefined' && mdnsAdvertiser) {
1216
- try { mdnsAdvertiser.destroy(); } catch (e) { }
1217
- }
1218
-
1219
1147
  // stop accepting new connections
1220
1148
  server.close(() => {
1221
1149
  console.log("HTTP server closed");