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.
Files changed (2) hide show
  1. package/dist/bin.js +273 -59
  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));
@@ -195,32 +262,29 @@ var RelayBridge = class {
195
262
  connectTimeout = null;
196
263
  encoder = new TextEncoder();
197
264
  decoder = new TextDecoder();
198
- firstConnect = true;
199
- async connect() {
265
+ sessionId = null;
266
+ async connect(onUrl) {
200
267
  if (this.destroyed) return;
201
- let sessionId;
202
- try {
203
- sessionId = await createSession(this.relay);
204
- } catch {
205
- if (this.firstConnect) {
206
- console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
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.firstConnect = false;
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
- var options = {};
331
- for (let i = 0; i < args.length; i++) {
332
- if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
333
- options.session = args[++i];
334
- } else if (args[i] === "--relay" && args[i + 1]) {
335
- options.relay = args[++i];
336
- } else {
337
- 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);
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
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fied",
3
- "version": "0.1.5",
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": {