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.
Files changed (2) hide show
  1. package/dist/bin.js +264 -44
  2. 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(options2) {
114
- const relay = options2.relay ?? DEFAULT_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 (options2.session) {
179
+ if (options.session) {
122
180
  const found = sessions.find(
123
- (s) => s.name === options2.session || s.id === options2.session
181
+ (s) => s.name === options.session || s.id === options.session
124
182
  );
125
183
  if (!found) {
126
- console.error(`tmux session "${options2.session}" not found.`);
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 = options2.cols ?? process.stdout.columns ?? 80;
144
- const rows = options2.rows ?? process.stdout.rows ?? 24;
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
- console.log("");
150
- console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
151
- console.log("");
152
- console.log(` Session: ${targetSession}`);
153
- console.log(` Size: ${cols}x${rows}`);
154
- console.log("");
155
- const bridge = new RelayBridge(relay, cryptoKey, keyFragment, pty);
156
- await bridge.connect();
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
- console.log(` \x1B[1mShare this link:\x1B[0m`);
211
- console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
212
- console.log("");
213
- console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
214
- console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
215
- console.log("");
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
- var options = {};
325
- for (let i = 0; i < args.length; i++) {
326
- if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
327
- options.session = args[++i];
328
- } else if (args[i] === "--relay" && args[i + 1]) {
329
- options.relay = args[++i];
330
- } else {
331
- console.error(`Unknown option: ${args[i]}`);
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
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fied",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Share your tmux session in the browser with end-to-end encryption",
5
5
  "type": "module",
6
6
  "bin": {