Skip to content

Commit 96a2a1a

Browse files
committed
feat: add instbyte watch — real-time clipboard sync from any channel
1 parent 7854979 commit 96a2a1a

File tree

5 files changed

+264
-2
lines changed

5 files changed

+264
-2
lines changed

bin/cli/watch.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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();

nodemon.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ignore": ["instbyte-data/", "uploads/"]
3+
}

package-lock.json

Lines changed: 85 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"multer": "^2.0.2",
4646
"sharp": "^0.33.2",
4747
"socket.io": "^4.6.1",
48+
"socket.io-client": "^4.6.1",
4849
"sqlite3": "^5.1.6"
4950
},
5051
"devDependencies": {

tests/unit/send.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from 'vitest'
2+
3+
// Inlined from bin/cli/send.js — tests the binary stdin guard logic
4+
function isBinary(buf) {
5+
const limit = Math.min(buf.length, 512);
6+
for (let i = 0; i < limit; i++) {
7+
if (buf[i] === 0) return true;
8+
}
9+
return false;
10+
}
11+
12+
describe('isBinary', () => {
13+
it('returns false for empty buffer', () => {
14+
expect(isBinary(Buffer.alloc(0))).toBe(false)
15+
})
16+
17+
it('returns false for plain UTF-8 text', () => {
18+
expect(isBinary(Buffer.from('hello from terminal\n'))).toBe(false)
19+
})
20+
21+
it('returns false for multiline text', () => {
22+
expect(isBinary(Buffer.from('line one\nline two\nline three\n'))).toBe(false)
23+
})
24+
25+
it('returns true when null byte is at the start', () => {
26+
expect(isBinary(Buffer.from([0x00, 0x01, 0x02]))).toBe(true)
27+
})
28+
29+
it('returns true when null byte is within the first 512 bytes', () => {
30+
const buf = Buffer.alloc(100, 0x41) // 'A' * 100
31+
buf[50] = 0x00
32+
expect(isBinary(buf)).toBe(true)
33+
})
34+
35+
it('returns false when null byte is beyond the 512-byte sample window', () => {
36+
const buf = Buffer.alloc(600, 0x41) // 'A' * 600
37+
buf[513] = 0x00 // null byte outside the check window
38+
expect(isBinary(buf)).toBe(false)
39+
})
40+
41+
it('returns true for a buffer that starts with common binary magic bytes', () => {
42+
// ZIP magic number — PK\x03\x04
43+
const zip = Buffer.from([0x50, 0x4B, 0x03, 0x04, 0x00])
44+
expect(isBinary(zip)).toBe(true)
45+
})
46+
47+
it('returns false for JSON content', () => {
48+
const json = Buffer.from(JSON.stringify({ key: 'value', num: 42 }))
49+
expect(isBinary(json)).toBe(false)
50+
})
51+
})

0 commit comments

Comments
 (0)