instbyte 1.11.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.11.1",
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": {
@@ -45,6 +45,7 @@
45
45
  "multer": "^2.0.2",
46
46
  "sharp": "^0.33.2",
47
47
  "socket.io": "^4.6.1",
48
+ "socket.io-client": "^4.6.1",
48
49
  "sqlite3": "^5.1.6"
49
50
  },
50
51
  "devDependencies": {
package/server/server.js CHANGED
@@ -855,7 +855,8 @@ app.get("/health", (req, res) => {
855
855
  res.json({
856
856
  status: "ok",
857
857
  uptime: Math.floor(process.uptime()),
858
- version: require("../package.json").version
858
+ version: require("../package.json").version,
859
+ connected: connectedUsers
859
860
  });
860
861
  });
861
862
 
@@ -1092,6 +1093,23 @@ function listenOnFreePort(server, preferredPort) {
1092
1093
  });
1093
1094
  }
1094
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
+
1095
1113
  const PREFERRED = parseInt(process.env.PORT) || config.server.port;
1096
1114
 
1097
1115
  const localIP = getLocalIP();
@@ -1109,6 +1127,7 @@ if (process.env.INSTBYTE_BOOT === '1') {
1109
1127
  }
1110
1128
 
1111
1129
  console.log("");
1130
+ writeRuntimeConfig(localIP, PORT);
1112
1131
  scanOrphans();
1113
1132
  }).catch(err => {
1114
1133
  console.error("Failed to start server:", err.message);