fied 0.1.6 → 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 +264 -44
- 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));
|
|
@@ -196,23 +263,26 @@ var RelayBridge = class {
|
|
|
196
263
|
encoder = new TextEncoder();
|
|
197
264
|
decoder = new TextDecoder();
|
|
198
265
|
sessionId = null;
|
|
199
|
-
async connect() {
|
|
266
|
+
async connect(onUrl) {
|
|
200
267
|
if (this.destroyed) return;
|
|
201
268
|
if (!this.sessionId) {
|
|
202
269
|
try {
|
|
203
270
|
this.sessionId = await createSession(this.relay);
|
|
204
271
|
} catch {
|
|
205
|
-
console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
272
|
+
if (!this.silent) console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
206
273
|
this.scheduleReconnect();
|
|
207
274
|
return;
|
|
208
275
|
}
|
|
209
276
|
const url = `${this.relay}/s/${this.sessionId}#${this.keyFragment}`;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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("");
|
|
285
|
+
}
|
|
216
286
|
}
|
|
217
287
|
const wsUrl = this.relay.replace(/^http/, "ws") + `/api/sessions/${this.sessionId}/ws?role=host`;
|
|
218
288
|
const ws = new WebSocket(wsUrl);
|
|
@@ -259,7 +329,7 @@ var RelayBridge = class {
|
|
|
259
329
|
}
|
|
260
330
|
this.ws = null;
|
|
261
331
|
if (!this.destroyed) {
|
|
262
|
-
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
332
|
+
if (!this.silent) console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
263
333
|
this.scheduleReconnect();
|
|
264
334
|
}
|
|
265
335
|
});
|
|
@@ -300,6 +370,62 @@ var RelayBridge = class {
|
|
|
300
370
|
}
|
|
301
371
|
};
|
|
302
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
|
+
|
|
303
429
|
// src/bin.ts
|
|
304
430
|
var args = process.argv.slice(2);
|
|
305
431
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -321,18 +447,112 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
321
447
|
`);
|
|
322
448
|
process.exit(0);
|
|
323
449
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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);
|
|
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
|
+
}
|
|
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");
|
|
332
516
|
process.exit(1);
|
|
333
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`;
|
|
334
558
|
}
|
|
335
|
-
share(options).catch((err) => {
|
|
336
|
-
console.error("Fatal:", err.message ?? err);
|
|
337
|
-
process.exit(1);
|
|
338
|
-
});
|