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 +16 -4
- package/bin/cli/resolve.js +55 -0
- package/bin/cli/send.js +161 -0
- package/bin/cli/status.js +67 -0
- package/bin/cli/watch.js +124 -0
- package/bin/instbyte.js +76 -21
- package/client/js/app.js +19 -14
- package/package.json +2 -2
- package/server/server.js +20 -92
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
|
|
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
|
-
|
|
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 (
|
|
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 };
|
package/bin/cli/send.js
ADDED
|
@@ -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();
|
package/bin/cli/watch.js
ADDED
|
@@ -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
|
|
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
|
-
//
|
|
14
|
+
// HELP
|
|
10
15
|
// ========================
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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 (!
|
|
31
|
-
|
|
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
|
-
//
|
|
73
|
+
// SERVER START
|
|
35
74
|
// ========================
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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");
|