fied 0.1.5 → 0.2.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/dist/bin.js +273 -59
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
3
7
|
// src/index.ts
|
|
4
8
|
import WebSocket from "ws";
|
|
5
9
|
|
|
@@ -103,6 +107,60 @@ function attachSession(sessionName, cols, rows) {
|
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
// src/store.ts
|
|
111
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
112
|
+
import { join } from "node:path";
|
|
113
|
+
import { homedir } from "node:os";
|
|
114
|
+
var STORE_DIR = join(homedir(), ".fied");
|
|
115
|
+
var STORE_FILE = join(STORE_DIR, "sessions.json");
|
|
116
|
+
function ensureDir() {
|
|
117
|
+
mkdirSync(STORE_DIR, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
function isAlive(pid) {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function loadSessions() {
|
|
128
|
+
try {
|
|
129
|
+
const raw = readFileSync(STORE_FILE, "utf-8");
|
|
130
|
+
const entries = JSON.parse(raw);
|
|
131
|
+
const alive = entries.filter((e) => isAlive(e.pid));
|
|
132
|
+
if (alive.length !== entries.length) {
|
|
133
|
+
saveSessions(alive);
|
|
134
|
+
}
|
|
135
|
+
return alive;
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function saveSessions(entries) {
|
|
141
|
+
ensureDir();
|
|
142
|
+
writeFileSync(STORE_FILE, JSON.stringify(entries, null, 2));
|
|
143
|
+
}
|
|
144
|
+
function addSession(entry) {
|
|
145
|
+
const entries = loadSessions();
|
|
146
|
+
entries.push(entry);
|
|
147
|
+
saveSessions(entries);
|
|
148
|
+
}
|
|
149
|
+
function removeSession(pid) {
|
|
150
|
+
const entries = loadSessions();
|
|
151
|
+
saveSessions(entries.filter((e) => e.pid !== pid));
|
|
152
|
+
}
|
|
153
|
+
function stopSession(pid) {
|
|
154
|
+
try {
|
|
155
|
+
process.kill(pid, "SIGTERM");
|
|
156
|
+
removeSession(pid);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
removeSession(pid);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
106
164
|
// src/index.ts
|
|
107
165
|
var DEFAULT_RELAY = "https://fied.app";
|
|
108
166
|
var MSG_TERMINAL_OUTPUT = 1;
|
|
@@ -110,24 +168,20 @@ var MSG_TERMINAL_INPUT = 2;
|
|
|
110
168
|
var MSG_RESIZE = 3;
|
|
111
169
|
var RECONNECT_BASE_MS = 1e3;
|
|
112
170
|
var RECONNECT_MAX_MS = 3e4;
|
|
113
|
-
async function share(
|
|
114
|
-
const relay =
|
|
171
|
+
async function share(options) {
|
|
172
|
+
const relay = options.relay ?? DEFAULT_RELAY;
|
|
115
173
|
const sessions = listSessions();
|
|
116
174
|
if (sessions.length === 0) {
|
|
117
175
|
console.error("No tmux sessions found. Start one with: tmux new -s mysession");
|
|
118
176
|
process.exit(1);
|
|
119
177
|
}
|
|
120
178
|
let targetSession;
|
|
121
|
-
if (
|
|
179
|
+
if (options.session) {
|
|
122
180
|
const found = sessions.find(
|
|
123
|
-
(s) => s.name ===
|
|
181
|
+
(s) => s.name === options.session || s.id === options.session
|
|
124
182
|
);
|
|
125
183
|
if (!found) {
|
|
126
|
-
console.error(`tmux session "${
|
|
127
|
-
console.error("Available sessions:");
|
|
128
|
-
for (const s of sessions) {
|
|
129
|
-
console.error(` ${s.name} (${s.windows} windows${s.attached ? ", attached" : ""})`);
|
|
130
|
-
}
|
|
184
|
+
console.error(`tmux session "${options.session}" not found.`);
|
|
131
185
|
process.exit(1);
|
|
132
186
|
}
|
|
133
187
|
targetSession = found.name;
|
|
@@ -135,34 +189,46 @@ async function share(options2) {
|
|
|
135
189
|
targetSession = sessions[0].name;
|
|
136
190
|
} else {
|
|
137
191
|
console.error("Multiple tmux sessions found. Specify one with --session:");
|
|
138
|
-
for (const s of sessions) {
|
|
139
|
-
console.error(` ${s.name} (${s.windows} windows${s.attached ? ", attached" : ""})`);
|
|
140
|
-
}
|
|
141
192
|
process.exit(1);
|
|
142
193
|
}
|
|
143
|
-
const cols =
|
|
144
|
-
const rows =
|
|
194
|
+
const cols = options.cols ?? process.stdout.columns ?? 80;
|
|
195
|
+
const rows = options.rows ?? process.stdout.rows ?? 24;
|
|
145
196
|
const rawKey = await generateKey();
|
|
146
197
|
const cryptoKey = await importKey(rawKey);
|
|
147
198
|
const keyFragment = toBase64Url(rawKey);
|
|
148
199
|
const pty = attachSession(targetSession, cols, rows);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
200
|
+
if (!options.background) {
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
|
|
203
|
+
console.log("");
|
|
204
|
+
console.log(` Session: ${targetSession}`);
|
|
205
|
+
console.log(` Size: ${cols}x${rows}`);
|
|
206
|
+
console.log("");
|
|
207
|
+
}
|
|
208
|
+
const bridge = new RelayBridge(relay, cryptoKey, keyFragment, pty, options.background);
|
|
209
|
+
const onUrl = (url) => {
|
|
210
|
+
if (options.background) {
|
|
211
|
+
addSession({
|
|
212
|
+
pid: process.pid,
|
|
213
|
+
tmuxSession: targetSession,
|
|
214
|
+
url,
|
|
215
|
+
relay,
|
|
216
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
await bridge.connect(onUrl);
|
|
157
221
|
const shutdown = () => {
|
|
158
222
|
bridge.destroy();
|
|
159
223
|
pty.kill();
|
|
224
|
+
if (options.background) removeSession(process.pid);
|
|
160
225
|
};
|
|
161
226
|
process.on("SIGINT", shutdown);
|
|
162
227
|
process.on("SIGTERM", shutdown);
|
|
163
228
|
await new Promise((resolve) => {
|
|
164
229
|
pty.onExit(() => {
|
|
165
230
|
bridge.destroy();
|
|
231
|
+
if (options.background) removeSession(process.pid);
|
|
166
232
|
resolve();
|
|
167
233
|
});
|
|
168
234
|
});
|
|
@@ -177,11 +243,12 @@ async function createSession(relay) {
|
|
|
177
243
|
}
|
|
178
244
|
var WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
179
245
|
var RelayBridge = class {
|
|
180
|
-
constructor(relay, key, keyFragment, pty) {
|
|
246
|
+
constructor(relay, key, keyFragment, pty, silent = false) {
|
|
181
247
|
this.relay = relay;
|
|
182
248
|
this.key = key;
|
|
183
249
|
this.keyFragment = keyFragment;
|
|
184
250
|
this.pty = pty;
|
|
251
|
+
this.silent = silent;
|
|
185
252
|
this.pty.onData((data) => {
|
|
186
253
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
187
254
|
this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
|
|
@@ -195,32 +262,29 @@ var RelayBridge = class {
|
|
|
195
262
|
connectTimeout = null;
|
|
196
263
|
encoder = new TextEncoder();
|
|
197
264
|
decoder = new TextDecoder();
|
|
198
|
-
|
|
199
|
-
async connect() {
|
|
265
|
+
sessionId = null;
|
|
266
|
+
async connect(onUrl) {
|
|
200
267
|
if (this.destroyed) return;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
268
|
+
if (!this.sessionId) {
|
|
269
|
+
try {
|
|
270
|
+
this.sessionId = await createSession(this.relay);
|
|
271
|
+
} catch {
|
|
272
|
+
if (!this.silent) console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
273
|
+
this.scheduleReconnect();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const url = `${this.relay}/s/${this.sessionId}#${this.keyFragment}`;
|
|
277
|
+
onUrl?.(url);
|
|
278
|
+
if (!this.silent) {
|
|
279
|
+
console.log(` \x1B[1mShare this link:\x1B[0m`);
|
|
280
|
+
console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
283
|
+
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
284
|
+
console.log("");
|
|
207
285
|
}
|
|
208
|
-
this.scheduleReconnect();
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const url = `${this.relay}/s/${sessionId}#${this.keyFragment}`;
|
|
212
|
-
if (this.firstConnect) {
|
|
213
|
-
console.log(` \x1B[1mShare this link:\x1B[0m`);
|
|
214
|
-
console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
215
|
-
console.log("");
|
|
216
|
-
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
217
|
-
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
218
|
-
console.log("");
|
|
219
|
-
} else {
|
|
220
|
-
console.error(` \x1B[32mReconnected.\x1B[0m New link: \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
221
286
|
}
|
|
222
|
-
this.
|
|
223
|
-
const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${sessionId}/ws?role=host`;
|
|
287
|
+
const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${this.sessionId}/ws?role=host`;
|
|
224
288
|
const ws = new WebSocket(wsUrl);
|
|
225
289
|
ws.binaryType = "arraybuffer";
|
|
226
290
|
this.ws = ws;
|
|
@@ -265,7 +329,7 @@ var RelayBridge = class {
|
|
|
265
329
|
}
|
|
266
330
|
this.ws = null;
|
|
267
331
|
if (!this.destroyed) {
|
|
268
|
-
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
332
|
+
if (!this.silent) console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
269
333
|
this.scheduleReconnect();
|
|
270
334
|
}
|
|
271
335
|
});
|
|
@@ -306,6 +370,62 @@ var RelayBridge = class {
|
|
|
306
370
|
}
|
|
307
371
|
};
|
|
308
372
|
|
|
373
|
+
// src/prompt.ts
|
|
374
|
+
import { createInterface } from "node:readline";
|
|
375
|
+
function ask(question) {
|
|
376
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
rl.question(question, (answer) => {
|
|
379
|
+
rl.close();
|
|
380
|
+
resolve(answer.trim());
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async function pickSession(names) {
|
|
385
|
+
console.error("");
|
|
386
|
+
console.error(" \x1B[1mSelect a tmux session:\x1B[0m");
|
|
387
|
+
console.error("");
|
|
388
|
+
for (let i = 0; i < names.length; i++) {
|
|
389
|
+
console.error(` \x1B[36m${i + 1}\x1B[0m) ${names[i]}`);
|
|
390
|
+
}
|
|
391
|
+
console.error("");
|
|
392
|
+
while (true) {
|
|
393
|
+
const answer = await ask(" Choice: ");
|
|
394
|
+
const idx = parseInt(answer, 10) - 1;
|
|
395
|
+
if (idx >= 0 && idx < names.length) {
|
|
396
|
+
return names[idx];
|
|
397
|
+
}
|
|
398
|
+
console.error(` \x1B[33mEnter a number between 1 and ${names.length}\x1B[0m`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function confirm(question, defaultYes = false) {
|
|
402
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
403
|
+
const answer = await ask(` ${question} [${hint}]: `);
|
|
404
|
+
if (answer === "") return defaultYes;
|
|
405
|
+
return answer.toLowerCase().startsWith("y");
|
|
406
|
+
}
|
|
407
|
+
async function pickManageAction(count) {
|
|
408
|
+
console.error("");
|
|
409
|
+
console.error(` \x1B[36mn\x1B[0m) Start new session`);
|
|
410
|
+
console.error(` \x1B[36ms\x1B[0m) Stop a session`);
|
|
411
|
+
console.error(` \x1B[36mq\x1B[0m) Quit`);
|
|
412
|
+
console.error("");
|
|
413
|
+
while (true) {
|
|
414
|
+
const answer = await ask(" Action: ");
|
|
415
|
+
const a = answer.toLowerCase();
|
|
416
|
+
if (a === "n" || a === "s" || a === "q") return a;
|
|
417
|
+
const idx = parseInt(answer, 10);
|
|
418
|
+
if (idx >= 1 && idx <= count) return `stop:${idx}`;
|
|
419
|
+
console.error(" \x1B[33mEnter n, s, or q\x1B[0m");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function pickStop(count) {
|
|
423
|
+
const answer = await ask(" Stop which session #: ");
|
|
424
|
+
const idx = parseInt(answer, 10) - 1;
|
|
425
|
+
if (idx >= 0 && idx < count) return idx;
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
309
429
|
// src/bin.ts
|
|
310
430
|
var args = process.argv.slice(2);
|
|
311
431
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -327,18 +447,112 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
327
447
|
`);
|
|
328
448
|
process.exit(0);
|
|
329
449
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
450
|
+
if (args.includes("--__daemon")) {
|
|
451
|
+
const options = {};
|
|
452
|
+
for (let i = 0; i < args.length; i++) {
|
|
453
|
+
if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
|
|
454
|
+
options.session = args[++i];
|
|
455
|
+
} else if (args[i] === "--relay" && args[i + 1]) {
|
|
456
|
+
options.relay = args[++i];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
share({ ...options, background: true }).catch(() => process.exit(1));
|
|
460
|
+
} else {
|
|
461
|
+
main().catch((err) => {
|
|
462
|
+
console.error("Fatal:", err.message ?? err);
|
|
338
463
|
process.exit(1);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function main() {
|
|
467
|
+
let relay;
|
|
468
|
+
let session;
|
|
469
|
+
for (let i = 0; i < args.length; i++) {
|
|
470
|
+
if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
|
|
471
|
+
session = args[++i];
|
|
472
|
+
} else if (args[i] === "--relay" && args[i + 1]) {
|
|
473
|
+
relay = args[++i];
|
|
474
|
+
} else if (!args[i].startsWith("-")) {
|
|
475
|
+
continue;
|
|
476
|
+
} else {
|
|
477
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
339
480
|
}
|
|
481
|
+
const active = loadSessions();
|
|
482
|
+
if (active.length > 0 && !session) {
|
|
483
|
+
console.error("");
|
|
484
|
+
console.error(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 active sessions");
|
|
485
|
+
console.error("");
|
|
486
|
+
for (let i = 0; i < active.length; i++) {
|
|
487
|
+
const s = active[i];
|
|
488
|
+
const age = timeSince(new Date(s.startedAt));
|
|
489
|
+
console.error(` \x1B[36m${i + 1}\x1B[0m) \x1B[1m${s.tmuxSession}\x1B[0m ${age} ago`);
|
|
490
|
+
console.error(` \x1B[4m\x1B[36m${s.url}\x1B[0m`);
|
|
491
|
+
}
|
|
492
|
+
const action = await pickManageAction(active.length);
|
|
493
|
+
if (action === "q") {
|
|
494
|
+
process.exit(0);
|
|
495
|
+
}
|
|
496
|
+
if (action === "s") {
|
|
497
|
+
const idx = await pickStop(active.length);
|
|
498
|
+
if (idx !== null) {
|
|
499
|
+
const entry = active[idx];
|
|
500
|
+
stopSession(entry.pid);
|
|
501
|
+
console.error(` \x1B[32mStopped\x1B[0m ${entry.tmuxSession}`);
|
|
502
|
+
}
|
|
503
|
+
process.exit(0);
|
|
504
|
+
}
|
|
505
|
+
if (action.startsWith("stop:")) {
|
|
506
|
+
const idx = parseInt(action.split(":")[1], 10) - 1;
|
|
507
|
+
const entry = active[idx];
|
|
508
|
+
stopSession(entry.pid);
|
|
509
|
+
console.error(` \x1B[32mStopped\x1B[0m ${entry.tmuxSession}`);
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const tmuxSessions = listSessions();
|
|
514
|
+
if (tmuxSessions.length === 0) {
|
|
515
|
+
console.error("No tmux sessions found. Start one with: tmux new -s mysession");
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
if (!session) {
|
|
519
|
+
if (tmuxSessions.length === 1) {
|
|
520
|
+
session = tmuxSessions[0].name;
|
|
521
|
+
} else {
|
|
522
|
+
session = await pickSession(tmuxSessions.map((s) => {
|
|
523
|
+
const tag = s.attached ? " \x1B[2m(attached)\x1B[0m" : "";
|
|
524
|
+
return `${s.name} \u2014 ${s.windows} window${s.windows !== 1 ? "s" : ""}${tag}`;
|
|
525
|
+
}));
|
|
526
|
+
session = session.split(" \u2014 ")[0].trim();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const background = await confirm("Run in background?");
|
|
530
|
+
if (background) {
|
|
531
|
+
const binPath = fileURLToPath(import.meta.url);
|
|
532
|
+
const childArgs = ["--__daemon", "--session", session];
|
|
533
|
+
if (relay) childArgs.push("--relay", relay);
|
|
534
|
+
const child = spawnChild(process.execPath, [binPath, ...childArgs], {
|
|
535
|
+
detached: true,
|
|
536
|
+
stdio: "ignore"
|
|
537
|
+
});
|
|
538
|
+
child.unref();
|
|
539
|
+
console.error("");
|
|
540
|
+
console.error(` \x1B[1m\x1B[32mfied\x1B[0m \u2014 started in background (PID ${child.pid})`);
|
|
541
|
+
console.error(` Session: ${session}`);
|
|
542
|
+
console.error(" Run \x1B[1mnpx fied\x1B[0m again to manage.");
|
|
543
|
+
console.error("");
|
|
544
|
+
setTimeout(() => process.exit(0), 500);
|
|
545
|
+
} else {
|
|
546
|
+
await share({ session, relay });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function timeSince(date) {
|
|
550
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
|
|
551
|
+
if (seconds < 60) return `${seconds}s`;
|
|
552
|
+
const minutes = Math.floor(seconds / 60);
|
|
553
|
+
if (minutes < 60) return `${minutes}m`;
|
|
554
|
+
const hours = Math.floor(minutes / 60);
|
|
555
|
+
if (hours < 24) return `${hours}h`;
|
|
556
|
+
const days = Math.floor(hours / 24);
|
|
557
|
+
return `${days}d`;
|
|
340
558
|
}
|
|
341
|
-
share(options).catch((err) => {
|
|
342
|
-
console.error("Fatal:", err.message ?? err);
|
|
343
|
-
process.exit(1);
|
|
344
|
-
});
|