fied 0.1.6 → 0.2.1
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 +373 -60
- 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,31 +107,89 @@ 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;
|
|
109
167
|
var MSG_TERMINAL_INPUT = 2;
|
|
110
168
|
var MSG_RESIZE = 3;
|
|
169
|
+
var RESIZE_MIN_COLS = 20;
|
|
170
|
+
var RESIZE_MAX_COLS = 1e3;
|
|
171
|
+
var RESIZE_MIN_ROWS = 5;
|
|
172
|
+
var RESIZE_MAX_ROWS = 300;
|
|
173
|
+
var MAX_INVALID_RESIZE_FRAMES = 5;
|
|
111
174
|
var RECONNECT_BASE_MS = 1e3;
|
|
112
175
|
var RECONNECT_MAX_MS = 3e4;
|
|
113
|
-
async function share(
|
|
114
|
-
const
|
|
176
|
+
async function share(options) {
|
|
177
|
+
const relayTarget = parseRelayTarget(
|
|
178
|
+
options.relay ?? DEFAULT_RELAY,
|
|
179
|
+
options.allowInsecureRelay ?? process.env.FIED_ALLOW_INSECURE_RELAY === "1"
|
|
180
|
+
);
|
|
115
181
|
const sessions = listSessions();
|
|
116
182
|
if (sessions.length === 0) {
|
|
117
183
|
console.error("No tmux sessions found. Start one with: tmux new -s mysession");
|
|
118
184
|
process.exit(1);
|
|
119
185
|
}
|
|
120
186
|
let targetSession;
|
|
121
|
-
if (
|
|
187
|
+
if (options.session) {
|
|
122
188
|
const found = sessions.find(
|
|
123
|
-
(s) => s.name ===
|
|
189
|
+
(s) => s.name === options.session || s.id === options.session
|
|
124
190
|
);
|
|
125
191
|
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
|
-
}
|
|
192
|
+
console.error(`tmux session "${options.session}" not found.`);
|
|
131
193
|
process.exit(1);
|
|
132
194
|
}
|
|
133
195
|
targetSession = found.name;
|
|
@@ -135,40 +197,91 @@ async function share(options2) {
|
|
|
135
197
|
targetSession = sessions[0].name;
|
|
136
198
|
} else {
|
|
137
199
|
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
200
|
process.exit(1);
|
|
142
201
|
}
|
|
143
|
-
const cols =
|
|
144
|
-
const rows =
|
|
202
|
+
const cols = options.cols ?? process.stdout.columns ?? 80;
|
|
203
|
+
const rows = options.rows ?? process.stdout.rows ?? 24;
|
|
145
204
|
const rawKey = await generateKey();
|
|
146
205
|
const cryptoKey = await importKey(rawKey);
|
|
147
206
|
const keyFragment = toBase64Url(rawKey);
|
|
148
207
|
const pty = attachSession(targetSession, cols, rows);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
208
|
+
if (!options.background) {
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 encrypted terminal sharing");
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(` Session: ${targetSession}`);
|
|
213
|
+
console.log(` Size: ${cols}x${rows}`);
|
|
214
|
+
console.log("");
|
|
215
|
+
}
|
|
216
|
+
const bridge = new RelayBridge(relayTarget, cryptoKey, keyFragment, pty, options.background);
|
|
217
|
+
const onUrl = (url) => {
|
|
218
|
+
if (options.background) {
|
|
219
|
+
addSession({
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
tmuxSession: targetSession,
|
|
222
|
+
url,
|
|
223
|
+
relay: relayTarget.httpBase.toString(),
|
|
224
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
await bridge.connect(onUrl);
|
|
229
|
+
let closed = false;
|
|
230
|
+
const cleanup = () => {
|
|
231
|
+
if (closed) return;
|
|
232
|
+
closed = true;
|
|
158
233
|
bridge.destroy();
|
|
159
|
-
|
|
234
|
+
try {
|
|
235
|
+
pty.kill();
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
if (options.background) {
|
|
239
|
+
removeSession(process.pid);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const exitNow = (code) => {
|
|
243
|
+
cleanup();
|
|
244
|
+
process.exit(code);
|
|
160
245
|
};
|
|
161
|
-
process.
|
|
162
|
-
process.
|
|
246
|
+
process.once("SIGINT", () => exitNow(0));
|
|
247
|
+
process.once("SIGTERM", () => exitNow(0));
|
|
163
248
|
await new Promise((resolve) => {
|
|
164
249
|
pty.onExit(() => {
|
|
165
|
-
|
|
250
|
+
cleanup();
|
|
166
251
|
resolve();
|
|
167
252
|
});
|
|
168
253
|
});
|
|
169
254
|
}
|
|
170
|
-
|
|
171
|
-
|
|
255
|
+
function parseRelayTarget(relay, allowInsecureRelay) {
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = new URL(relay);
|
|
259
|
+
} catch {
|
|
260
|
+
throw new Error(`Invalid relay URL: ${relay}`);
|
|
261
|
+
}
|
|
262
|
+
const isHttps = parsed.protocol === "https:";
|
|
263
|
+
const isHttp = parsed.protocol === "http:";
|
|
264
|
+
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
|
|
265
|
+
if (!isHttps) {
|
|
266
|
+
if (!(isHttp && isLocalhost && allowInsecureRelay)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"Relay must use https://. For local development, use http://localhost with --allow-insecure-relay or FIED_ALLOW_INSECURE_RELAY=1"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const httpBase = new URL(parsed.toString());
|
|
273
|
+
httpBase.hash = "";
|
|
274
|
+
httpBase.search = "";
|
|
275
|
+
if (!httpBase.pathname.endsWith("/")) {
|
|
276
|
+
httpBase.pathname = `${httpBase.pathname}/`;
|
|
277
|
+
}
|
|
278
|
+
const wsBase = new URL(httpBase.toString());
|
|
279
|
+
wsBase.protocol = httpBase.protocol === "https:" ? "wss:" : "ws:";
|
|
280
|
+
return { httpBase, wsBase };
|
|
281
|
+
}
|
|
282
|
+
async function createSession(relayHttpBase) {
|
|
283
|
+
const url = new URL("api/sessions", relayHttpBase);
|
|
284
|
+
const res = await fetch(url.toString(), { method: "POST" });
|
|
172
285
|
if (!res.ok) {
|
|
173
286
|
throw new Error(`Failed to create session: ${res.status} ${res.statusText}`);
|
|
174
287
|
}
|
|
@@ -177,11 +290,12 @@ async function createSession(relay) {
|
|
|
177
290
|
}
|
|
178
291
|
var WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
179
292
|
var RelayBridge = class {
|
|
180
|
-
constructor(
|
|
181
|
-
this.
|
|
293
|
+
constructor(relayTarget, key, keyFragment, pty, silent = false) {
|
|
294
|
+
this.relayTarget = relayTarget;
|
|
182
295
|
this.key = key;
|
|
183
296
|
this.keyFragment = keyFragment;
|
|
184
297
|
this.pty = pty;
|
|
298
|
+
this.silent = silent;
|
|
185
299
|
this.pty.onData((data) => {
|
|
186
300
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
187
301
|
this.sendEncrypted(MSG_TERMINAL_OUTPUT, this.encoder.encode(data));
|
|
@@ -196,26 +310,36 @@ var RelayBridge = class {
|
|
|
196
310
|
encoder = new TextEncoder();
|
|
197
311
|
decoder = new TextDecoder();
|
|
198
312
|
sessionId = null;
|
|
199
|
-
|
|
313
|
+
onUrl = null;
|
|
314
|
+
invalidResizeFrames = 0;
|
|
315
|
+
async connect(onUrl) {
|
|
200
316
|
if (this.destroyed) return;
|
|
317
|
+
if (onUrl) {
|
|
318
|
+
this.onUrl = onUrl;
|
|
319
|
+
}
|
|
201
320
|
if (!this.sessionId) {
|
|
202
321
|
try {
|
|
203
|
-
this.sessionId = await createSession(this.
|
|
322
|
+
this.sessionId = await createSession(this.relayTarget.httpBase);
|
|
204
323
|
} catch {
|
|
205
|
-
console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
324
|
+
if (!this.silent) console.error(" \x1B[31mRelay unreachable, retrying...\x1B[0m");
|
|
206
325
|
this.scheduleReconnect();
|
|
207
326
|
return;
|
|
208
327
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
328
|
+
const shareUrl = new URL(`s/${this.sessionId}`, this.relayTarget.httpBase);
|
|
329
|
+
const url = `${shareUrl.toString()}#${this.keyFragment}`;
|
|
330
|
+
this.onUrl?.(url);
|
|
331
|
+
if (!this.silent) {
|
|
332
|
+
console.log(` \x1B[1mShare this link:\x1B[0m`);
|
|
333
|
+
console.log(` \x1B[4m\x1B[36m${url}\x1B[0m`);
|
|
334
|
+
console.log("");
|
|
335
|
+
console.log(" \x1B[2mThe encryption key is in the URL fragment (#) \u2014 the server never sees it.\x1B[0m");
|
|
336
|
+
console.log(" \x1B[2mPress Ctrl+C to stop sharing.\x1B[0m");
|
|
337
|
+
console.log("");
|
|
338
|
+
}
|
|
216
339
|
}
|
|
217
|
-
const wsUrl =
|
|
218
|
-
|
|
340
|
+
const wsUrl = new URL(`api/sessions/${this.sessionId}/ws`, this.relayTarget.wsBase);
|
|
341
|
+
wsUrl.searchParams.set("role", "host");
|
|
342
|
+
const ws = new WebSocket(wsUrl.toString());
|
|
219
343
|
ws.binaryType = "arraybuffer";
|
|
220
344
|
this.ws = ws;
|
|
221
345
|
this.connectTimeout = setTimeout(() => {
|
|
@@ -246,10 +370,22 @@ var RelayBridge = class {
|
|
|
246
370
|
this.pty.write(this.decoder.decode(plaintext));
|
|
247
371
|
} else if (frame.type === MSG_RESIZE) {
|
|
248
372
|
const plaintext = await decrypt(this.key, frame.iv, frame.ciphertext);
|
|
249
|
-
const
|
|
250
|
-
|
|
373
|
+
const resize = parseResizePayload(this.decoder.decode(plaintext));
|
|
374
|
+
if (!resize) {
|
|
375
|
+
this.invalidResizeFrames += 1;
|
|
376
|
+
if (this.invalidResizeFrames >= MAX_INVALID_RESIZE_FRAMES) {
|
|
377
|
+
ws.close(1008, "invalid resize frames");
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.invalidResizeFrames = 0;
|
|
382
|
+
this.pty.resize(resize.cols, resize.rows);
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
if (!this.silent) {
|
|
386
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
387
|
+
console.error(` \x1B[33mIncoming frame rejected:\x1B[0m ${detail}`);
|
|
251
388
|
}
|
|
252
|
-
} catch {
|
|
253
389
|
}
|
|
254
390
|
});
|
|
255
391
|
ws.on("close", () => {
|
|
@@ -259,7 +395,7 @@ var RelayBridge = class {
|
|
|
259
395
|
}
|
|
260
396
|
this.ws = null;
|
|
261
397
|
if (!this.destroyed) {
|
|
262
|
-
console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
398
|
+
if (!this.silent) console.error(" \x1B[33mConnection lost, reconnecting...\x1B[0m");
|
|
263
399
|
this.scheduleReconnect();
|
|
264
400
|
}
|
|
265
401
|
});
|
|
@@ -295,10 +431,86 @@ var RelayBridge = class {
|
|
|
295
431
|
const { iv, ciphertext } = await encrypt(this.key, plaintext);
|
|
296
432
|
const frame = frameMessage(type, iv, ciphertext);
|
|
297
433
|
this.ws.send(frame);
|
|
298
|
-
} catch {
|
|
434
|
+
} catch (err) {
|
|
435
|
+
if (!this.silent) {
|
|
436
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
437
|
+
console.error(` \x1B[33mEncryption failed:\x1B[0m ${detail}`);
|
|
438
|
+
}
|
|
299
439
|
}
|
|
300
440
|
}
|
|
301
441
|
};
|
|
442
|
+
function parseResizePayload(payload) {
|
|
443
|
+
let parsed;
|
|
444
|
+
try {
|
|
445
|
+
parsed = JSON.parse(payload);
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
450
|
+
const typed = parsed;
|
|
451
|
+
if (!Number.isInteger(typed.cols) || !Number.isInteger(typed.rows)) return null;
|
|
452
|
+
const cols = typed.cols;
|
|
453
|
+
const rows = typed.rows;
|
|
454
|
+
if (cols < RESIZE_MIN_COLS || cols > RESIZE_MAX_COLS) return null;
|
|
455
|
+
if (rows < RESIZE_MIN_ROWS || rows > RESIZE_MAX_ROWS) return null;
|
|
456
|
+
return { cols, rows };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/prompt.ts
|
|
460
|
+
import { createInterface } from "node:readline";
|
|
461
|
+
function ask(question) {
|
|
462
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
463
|
+
return new Promise((resolve) => {
|
|
464
|
+
rl.question(question, (answer) => {
|
|
465
|
+
rl.close();
|
|
466
|
+
resolve(answer.trim());
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async function pickSession(names) {
|
|
471
|
+
console.error("");
|
|
472
|
+
console.error(" \x1B[1mSelect a tmux session:\x1B[0m");
|
|
473
|
+
console.error("");
|
|
474
|
+
for (let i = 0; i < names.length; i++) {
|
|
475
|
+
console.error(` \x1B[36m${i + 1}\x1B[0m) ${names[i]}`);
|
|
476
|
+
}
|
|
477
|
+
console.error("");
|
|
478
|
+
while (true) {
|
|
479
|
+
const answer = await ask(" Choice: ");
|
|
480
|
+
const idx = parseInt(answer, 10) - 1;
|
|
481
|
+
if (idx >= 0 && idx < names.length) {
|
|
482
|
+
return names[idx];
|
|
483
|
+
}
|
|
484
|
+
console.error(` \x1B[33mEnter a number between 1 and ${names.length}\x1B[0m`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function confirm(question, defaultYes = false) {
|
|
488
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
489
|
+
const answer = await ask(` ${question} [${hint}]: `);
|
|
490
|
+
if (answer === "") return defaultYes;
|
|
491
|
+
return answer.toLowerCase().startsWith("y");
|
|
492
|
+
}
|
|
493
|
+
async function pickManageAction(count) {
|
|
494
|
+
console.error("");
|
|
495
|
+
console.error(` \x1B[36mn\x1B[0m) Start new session`);
|
|
496
|
+
console.error(` \x1B[36ms\x1B[0m) Stop a session`);
|
|
497
|
+
console.error(` \x1B[36mq\x1B[0m) Quit`);
|
|
498
|
+
console.error("");
|
|
499
|
+
while (true) {
|
|
500
|
+
const answer = await ask(" Action: ");
|
|
501
|
+
const a = answer.toLowerCase();
|
|
502
|
+
if (a === "n" || a === "s" || a === "q") return a;
|
|
503
|
+
const idx = parseInt(answer, 10);
|
|
504
|
+
if (idx >= 1 && idx <= count) return `stop:${idx}`;
|
|
505
|
+
console.error(" \x1B[33mEnter n, s, or q\x1B[0m");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function pickStop(count) {
|
|
509
|
+
const answer = await ask(" Stop which session #: ");
|
|
510
|
+
const idx = parseInt(answer, 10) - 1;
|
|
511
|
+
if (idx >= 0 && idx < count) return idx;
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
302
514
|
|
|
303
515
|
// src/bin.ts
|
|
304
516
|
var args = process.argv.slice(2);
|
|
@@ -312,6 +524,7 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
312
524
|
\x1B[1mOptions:\x1B[0m
|
|
313
525
|
--session, -s <name> tmux session to share (auto-detected if only one)
|
|
314
526
|
--relay <url> relay server URL (default: https://fied.app)
|
|
527
|
+
--allow-insecure-relay allow http://localhost relay (dev only)
|
|
315
528
|
--help, -h show this help
|
|
316
529
|
|
|
317
530
|
\x1B[1mExamples:\x1B[0m
|
|
@@ -321,18 +534,118 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
321
534
|
`);
|
|
322
535
|
process.exit(0);
|
|
323
536
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
537
|
+
if (args.includes("--__daemon")) {
|
|
538
|
+
const options = {};
|
|
539
|
+
for (let i = 0; i < args.length; i++) {
|
|
540
|
+
if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
|
|
541
|
+
options.session = args[++i];
|
|
542
|
+
} else if (args[i] === "--relay" && args[i + 1]) {
|
|
543
|
+
options.relay = args[++i];
|
|
544
|
+
} else if (args[i] === "--allow-insecure-relay") {
|
|
545
|
+
options.allowInsecureRelay = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
share({ ...options, background: true }).catch(() => process.exit(1));
|
|
549
|
+
} else {
|
|
550
|
+
main().catch((err) => {
|
|
551
|
+
console.error("Fatal:", err.message ?? err);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function main() {
|
|
556
|
+
let relay;
|
|
557
|
+
let session;
|
|
558
|
+
let allowInsecureRelay = false;
|
|
559
|
+
for (let i = 0; i < args.length; i++) {
|
|
560
|
+
if ((args[i] === "--session" || args[i] === "-s") && args[i + 1]) {
|
|
561
|
+
session = args[++i];
|
|
562
|
+
} else if (args[i] === "--relay" && args[i + 1]) {
|
|
563
|
+
relay = args[++i];
|
|
564
|
+
} else if (args[i] === "--allow-insecure-relay") {
|
|
565
|
+
allowInsecureRelay = true;
|
|
566
|
+
} else if (!args[i].startsWith("-")) {
|
|
567
|
+
continue;
|
|
568
|
+
} else {
|
|
569
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const active = loadSessions();
|
|
574
|
+
if (active.length > 0 && !session) {
|
|
575
|
+
console.error("");
|
|
576
|
+
console.error(" \x1B[1m\x1B[32mfied\x1B[0m \u2014 active sessions");
|
|
577
|
+
console.error("");
|
|
578
|
+
for (let i = 0; i < active.length; i++) {
|
|
579
|
+
const s = active[i];
|
|
580
|
+
const age = timeSince(new Date(s.startedAt));
|
|
581
|
+
console.error(` \x1B[36m${i + 1}\x1B[0m) \x1B[1m${s.tmuxSession}\x1B[0m ${age} ago`);
|
|
582
|
+
console.error(` \x1B[4m\x1B[36m${s.url}\x1B[0m`);
|
|
583
|
+
}
|
|
584
|
+
const action = await pickManageAction(active.length);
|
|
585
|
+
if (action === "q") {
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
if (action === "s") {
|
|
589
|
+
const idx = await pickStop(active.length);
|
|
590
|
+
if (idx !== null) {
|
|
591
|
+
const entry = active[idx];
|
|
592
|
+
stopSession(entry.pid);
|
|
593
|
+
console.error(` \x1B[32mStopped\x1B[0m ${entry.tmuxSession}`);
|
|
594
|
+
}
|
|
595
|
+
process.exit(0);
|
|
596
|
+
}
|
|
597
|
+
if (action.startsWith("stop:")) {
|
|
598
|
+
const idx = parseInt(action.split(":")[1], 10) - 1;
|
|
599
|
+
const entry = active[idx];
|
|
600
|
+
stopSession(entry.pid);
|
|
601
|
+
console.error(` \x1B[32mStopped\x1B[0m ${entry.tmuxSession}`);
|
|
602
|
+
process.exit(0);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const tmuxSessions = listSessions();
|
|
606
|
+
if (tmuxSessions.length === 0) {
|
|
607
|
+
console.error("No tmux sessions found. Start one with: tmux new -s mysession");
|
|
332
608
|
process.exit(1);
|
|
333
609
|
}
|
|
610
|
+
if (!session) {
|
|
611
|
+
if (tmuxSessions.length === 1) {
|
|
612
|
+
session = tmuxSessions[0].name;
|
|
613
|
+
} else {
|
|
614
|
+
session = await pickSession(tmuxSessions.map((s) => {
|
|
615
|
+
const tag = s.attached ? " \x1B[2m(attached)\x1B[0m" : "";
|
|
616
|
+
return `${s.name} \u2014 ${s.windows} window${s.windows !== 1 ? "s" : ""}${tag}`;
|
|
617
|
+
}));
|
|
618
|
+
session = session.split(" \u2014 ")[0].trim();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const background = await confirm("Run in background?");
|
|
622
|
+
if (background) {
|
|
623
|
+
const binPath = fileURLToPath(import.meta.url);
|
|
624
|
+
const childArgs = ["--__daemon", "--session", session];
|
|
625
|
+
if (relay) childArgs.push("--relay", relay);
|
|
626
|
+
if (allowInsecureRelay) childArgs.push("--allow-insecure-relay");
|
|
627
|
+
const child = spawnChild(process.execPath, [binPath, ...childArgs], {
|
|
628
|
+
detached: true,
|
|
629
|
+
stdio: "ignore"
|
|
630
|
+
});
|
|
631
|
+
child.unref();
|
|
632
|
+
console.error("");
|
|
633
|
+
console.error(` \x1B[1m\x1B[32mfied\x1B[0m \u2014 started in background (PID ${child.pid})`);
|
|
634
|
+
console.error(` Session: ${session}`);
|
|
635
|
+
console.error(" Run \x1B[1mnpx fied\x1B[0m again to manage.");
|
|
636
|
+
console.error("");
|
|
637
|
+
setTimeout(() => process.exit(0), 500);
|
|
638
|
+
} else {
|
|
639
|
+
await share({ session, relay, allowInsecureRelay });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function timeSince(date) {
|
|
643
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
|
|
644
|
+
if (seconds < 60) return `${seconds}s`;
|
|
645
|
+
const minutes = Math.floor(seconds / 60);
|
|
646
|
+
if (minutes < 60) return `${minutes}m`;
|
|
647
|
+
const hours = Math.floor(minutes / 60);
|
|
648
|
+
if (hours < 24) return `${hours}h`;
|
|
649
|
+
const days = Math.floor(hours / 24);
|
|
650
|
+
return `${days}d`;
|
|
334
651
|
}
|
|
335
|
-
share(options).catch((err) => {
|
|
336
|
-
console.error("Fatal:", err.message ?? err);
|
|
337
|
-
process.exit(1);
|
|
338
|
-
});
|