peer-term 1.1.7 → 1.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/package.json +2 -1
- package/src/index.js +153 -14
- package/src/ui.js +4 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* CLI flags:
|
|
12
12
|
* --expiry <value> Session code expiry (e.g. 5m, 30s, 1h). Default: 5m
|
|
13
|
+
* --rejoin <value> Reconnection window (e.g. 5m, 1h, 6h). Default: 2m
|
|
13
14
|
* --readonly Prevent client keystrokes from reaching the PTY
|
|
14
15
|
* --path <dir> Starting directory for the terminal session
|
|
15
16
|
* --relay <url> Custom relay server URL
|
|
@@ -52,11 +53,14 @@ dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
|
|
52
53
|
|
|
53
54
|
const argv = minimist(process.argv.slice(2), {
|
|
54
55
|
boolean: ['readonly', 'verbose', 'help', 'version'],
|
|
55
|
-
string: ['expiry', 'relay', 'path'],
|
|
56
|
+
string: ['expiry', 'rejoin', 'relay', 'path'],
|
|
56
57
|
alias: { h: 'help', v: 'version', V: 'verbose' },
|
|
57
|
-
default: { expiry: '5m' },
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Detect if --expiry or --rejoin were explicitly passed on the command line
|
|
61
|
+
const expiryFlagPassed = process.argv.some(a => a === '--expiry' || a.startsWith('--expiry='));
|
|
62
|
+
const rejoinFlagPassed = process.argv.some(a => a === '--rejoin' || a.startsWith('--rejoin='));
|
|
63
|
+
|
|
60
64
|
// Handle --help
|
|
61
65
|
if (argv.help) {
|
|
62
66
|
printHelp();
|
|
@@ -143,6 +147,130 @@ function getExpiry() {
|
|
|
143
147
|
return parsed;
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
// ─── Interactive Prompts ─────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Prompt the user for a duration value in the terminal.
|
|
154
|
+
* Re-prompts on invalid input until a valid value is entered or Enter is pressed for default.
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} opts
|
|
157
|
+
* @param {string} opts.label - Main prompt question
|
|
158
|
+
* @param {string} opts.description - Additional context shown below the question
|
|
159
|
+
* @param {number} opts.defaultMs - Default value in milliseconds
|
|
160
|
+
* @param {string} opts.defaultLabel - Human-readable default (e.g. "5 minutes")
|
|
161
|
+
* @param {number} opts.minMs - Minimum allowed value in milliseconds
|
|
162
|
+
* @param {number} opts.maxMs - Maximum allowed value in milliseconds
|
|
163
|
+
* @param {string} opts.minLabel - Human-readable minimum (e.g. "2m")
|
|
164
|
+
* @param {string} opts.maxLabel - Human-readable maximum (e.g. "24h")
|
|
165
|
+
* @returns {Promise<number>} Resolved duration in milliseconds
|
|
166
|
+
*/
|
|
167
|
+
function promptDuration({ label, description, defaultMs, defaultLabel, minMs, maxMs, minLabel, maxLabel }) {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const rl = readline.createInterface({
|
|
170
|
+
input: process.stdin,
|
|
171
|
+
output: process.stdout,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const ask = () => {
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(` ${label}`);
|
|
177
|
+
if (description) console.log(` ${description}`);
|
|
178
|
+
console.log(` Enter a value between ${minLabel} and ${maxLabel} (e.g. ${minLabel}, 30m, 2h)`);
|
|
179
|
+
console.log(` Default is ${defaultLabel}. Press Enter to use default.`);
|
|
180
|
+
rl.question(' > ', (answer) => {
|
|
181
|
+
const input = answer.trim();
|
|
182
|
+
|
|
183
|
+
// Empty input — use default
|
|
184
|
+
if (!input) {
|
|
185
|
+
rl.close();
|
|
186
|
+
resolve(defaultMs);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parsed = parseDuration(input);
|
|
191
|
+
if (!parsed || parsed < minMs || parsed > maxMs) {
|
|
192
|
+
console.log(` Invalid value. Please enter a time between ${minLabel} and ${maxLabel} (e.g. 5m, 1h).`);
|
|
193
|
+
ask();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rl.close();
|
|
198
|
+
resolve(parsed);
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ask();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get the session code expiry — from flag, interactive prompt, or silent default.
|
|
208
|
+
*/
|
|
209
|
+
async function getExpiryInteractive() {
|
|
210
|
+
const DEFAULT_EXPIRY = 5 * 60 * 1000; // 5 minutes
|
|
211
|
+
const MIN_EXPIRY = 2 * 60 * 1000; // 2 minutes
|
|
212
|
+
const MAX_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
|
|
213
|
+
|
|
214
|
+
// If --expiry flag was explicitly passed, use it (validate and exit on error)
|
|
215
|
+
if (expiryFlagPassed) {
|
|
216
|
+
const parsed = parseDuration(argv.expiry);
|
|
217
|
+
if (!parsed || parsed < MIN_EXPIRY || parsed > MAX_EXPIRY) {
|
|
218
|
+
logger.error(`Invalid expiry: "${argv.expiry}". Must be between 2m and 24h.`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Non-interactive — use default silently
|
|
225
|
+
if (!process.stdin.isTTY) return DEFAULT_EXPIRY;
|
|
226
|
+
|
|
227
|
+
// Interactive prompt
|
|
228
|
+
return promptDuration({
|
|
229
|
+
label: 'How long should the session code be valid?',
|
|
230
|
+
description: null,
|
|
231
|
+
defaultMs: DEFAULT_EXPIRY,
|
|
232
|
+
defaultLabel: '5 minutes',
|
|
233
|
+
minMs: MIN_EXPIRY,
|
|
234
|
+
maxMs: MAX_EXPIRY,
|
|
235
|
+
minLabel: '2m',
|
|
236
|
+
maxLabel: '24h',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the reconnection window — from flag, interactive prompt, or silent default.
|
|
242
|
+
*/
|
|
243
|
+
async function getRejoinInteractive() {
|
|
244
|
+
const DEFAULT_REJOIN = 2 * 60 * 1000; // 2 minutes
|
|
245
|
+
const MIN_REJOIN = 5 * 60 * 1000; // 5 minutes
|
|
246
|
+
const MAX_REJOIN = 6 * 60 * 60 * 1000; // 6 hours
|
|
247
|
+
|
|
248
|
+
// If --rejoin flag was explicitly passed, use it
|
|
249
|
+
if (rejoinFlagPassed) {
|
|
250
|
+
const parsed = parseDuration(argv.rejoin);
|
|
251
|
+
if (!parsed || parsed < MIN_REJOIN || parsed > MAX_REJOIN) {
|
|
252
|
+
logger.error(`Invalid rejoin window: "${argv.rejoin}". Must be between 5m and 6h.`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
return parsed;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Non-interactive — use default silently
|
|
259
|
+
if (!process.stdin.isTTY) return DEFAULT_REJOIN;
|
|
260
|
+
|
|
261
|
+
// Interactive prompt
|
|
262
|
+
return promptDuration({
|
|
263
|
+
label: 'How long should the reconnection window be?',
|
|
264
|
+
description: 'This is how long a disconnected client or host has to rejoin before the session ends.',
|
|
265
|
+
defaultMs: DEFAULT_REJOIN,
|
|
266
|
+
defaultLabel: '2 minutes',
|
|
267
|
+
minMs: MIN_REJOIN,
|
|
268
|
+
maxMs: MAX_REJOIN,
|
|
269
|
+
minLabel: '5m',
|
|
270
|
+
maxLabel: '6h',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
146
274
|
// ─── Shell Detection ─────────────────────────────────────────────────────────
|
|
147
275
|
|
|
148
276
|
function detectShell() {
|
|
@@ -164,9 +292,10 @@ function detectShell() {
|
|
|
164
292
|
// ─── Session Class ───────────────────────────────────────────────────────────
|
|
165
293
|
|
|
166
294
|
class Session {
|
|
167
|
-
constructor(shell, expiryMs, readOnly, startPath, onDestroy) {
|
|
295
|
+
constructor(shell, expiryMs, rejoinMs, readOnly, startPath, onDestroy) {
|
|
168
296
|
this.shell = shell;
|
|
169
297
|
this.expiryMs = expiryMs;
|
|
298
|
+
this.rejoinMs = rejoinMs;
|
|
170
299
|
this.readOnly = readOnly;
|
|
171
300
|
this.startPath = startPath;
|
|
172
301
|
this.onDestroy = onDestroy;
|
|
@@ -212,7 +341,7 @@ class Session {
|
|
|
212
341
|
const config = JSON.stringify({
|
|
213
342
|
type: 'session-config',
|
|
214
343
|
readonly: this.readOnly,
|
|
215
|
-
version: '1.
|
|
344
|
+
version: '1.2.0',
|
|
216
345
|
shell: this.shell,
|
|
217
346
|
startPath: this.startPath,
|
|
218
347
|
});
|
|
@@ -252,6 +381,7 @@ class Session {
|
|
|
252
381
|
this.ws.send(JSON.stringify({
|
|
253
382
|
type: 'host-register',
|
|
254
383
|
expiry: this.expiryMs,
|
|
384
|
+
rejoinWindow: this.rejoinMs,
|
|
255
385
|
readonly: this.readOnly,
|
|
256
386
|
}));
|
|
257
387
|
});
|
|
@@ -272,6 +402,7 @@ class Session {
|
|
|
272
402
|
printSessionBox({
|
|
273
403
|
code: this.code,
|
|
274
404
|
expiry: formatDuration(this.expiryMs),
|
|
405
|
+
rejoinWindow: formatDuration(this.rejoinMs),
|
|
275
406
|
mode: this.readOnly ? 'Read-Only' : 'Read-Write',
|
|
276
407
|
shell: this.shell,
|
|
277
408
|
startPath: this.startPath,
|
|
@@ -362,7 +493,7 @@ class Session {
|
|
|
362
493
|
}
|
|
363
494
|
|
|
364
495
|
case 'peer-disconnected': {
|
|
365
|
-
this.log(
|
|
496
|
+
this.log(`Client disconnected. Rejoin window: ${formatDuration(this.rejoinMs)}.`);
|
|
366
497
|
this.isClientConnected = false;
|
|
367
498
|
this.awaitingRejoin = true;
|
|
368
499
|
this.sharedKey = null;
|
|
@@ -787,7 +918,7 @@ class Session {
|
|
|
787
918
|
}
|
|
788
919
|
|
|
789
920
|
case 'peer-disconnected': {
|
|
790
|
-
this.log(
|
|
921
|
+
this.log(`Client disconnected. Rejoin window: ${formatDuration(this.rejoinMs)}.`);
|
|
791
922
|
this.isClientConnected = false;
|
|
792
923
|
this.awaitingRejoin = true;
|
|
793
924
|
this.sharedKey = null;
|
|
@@ -958,16 +1089,17 @@ class Session {
|
|
|
958
1089
|
// ─── Session Manager ─────────────────────────────────────────────────────────
|
|
959
1090
|
|
|
960
1091
|
class SessionManager {
|
|
961
|
-
constructor(shell, expiryMs, readOnly, startPath) {
|
|
1092
|
+
constructor(shell, expiryMs, rejoinMs, readOnly, startPath) {
|
|
962
1093
|
this.shell = shell;
|
|
963
1094
|
this.expiryMs = expiryMs;
|
|
1095
|
+
this.rejoinMs = rejoinMs;
|
|
964
1096
|
this.readOnly = readOnly;
|
|
965
1097
|
this.startPath = startPath;
|
|
966
1098
|
this.sessions = new Map(); // code → Session
|
|
967
1099
|
}
|
|
968
1100
|
|
|
969
1101
|
async createSession() {
|
|
970
|
-
const session = new Session(this.shell, this.expiryMs, this.readOnly, this.startPath, (code) => {
|
|
1102
|
+
const session = new Session(this.shell, this.expiryMs, this.rejoinMs, this.readOnly, this.startPath, (code) => {
|
|
971
1103
|
this.sessions.delete(code);
|
|
972
1104
|
});
|
|
973
1105
|
|
|
@@ -1016,20 +1148,27 @@ class SessionManager {
|
|
|
1016
1148
|
|
|
1017
1149
|
async function main() {
|
|
1018
1150
|
const shell = detectShell();
|
|
1019
|
-
const expiryMs = getExpiry();
|
|
1020
1151
|
const readOnly = argv.readonly;
|
|
1021
1152
|
const startPath = resolveStartPath(argv.path);
|
|
1022
1153
|
|
|
1023
1154
|
// Print startup banner
|
|
1024
1155
|
printBanner();
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1156
|
+
|
|
1157
|
+
// Interactive prompts — always ask unless flags are passed or non-interactive
|
|
1158
|
+
const expiryMs = await getExpiryInteractive();
|
|
1159
|
+
const rejoinMs = await getRejoinInteractive();
|
|
1160
|
+
console.log('');
|
|
1161
|
+
|
|
1162
|
+
logger.info(`Shell: ${shell}`);
|
|
1163
|
+
logger.info(`Relays: ${RELAY_URLS.join(', ')}`);
|
|
1164
|
+
logger.info(`Path: ${startPath}`);
|
|
1165
|
+
logger.info(`Expiry: ${formatDuration(expiryMs)}`);
|
|
1166
|
+
logger.info(`Rejoin Window: ${formatDuration(rejoinMs)}`);
|
|
1167
|
+
if (readOnly) logger.info('Mode: READ-ONLY');
|
|
1029
1168
|
if (argv.verbose) logger.info('Verbose logging enabled');
|
|
1030
1169
|
console.log('');
|
|
1031
1170
|
|
|
1032
|
-
const manager = new SessionManager(shell, expiryMs, readOnly, startPath);
|
|
1171
|
+
const manager = new SessionManager(shell, expiryMs, rejoinMs, readOnly, startPath);
|
|
1033
1172
|
|
|
1034
1173
|
// Create first session automatically
|
|
1035
1174
|
const firstCode = await manager.createSession();
|
package/src/ui.js
CHANGED
|
@@ -47,7 +47,7 @@ export function printBanner() {
|
|
|
47
47
|
* @param {string} [opts.startPath] - Starting directory for the session
|
|
48
48
|
* @param {string} [opts.shareUrl] - URL to share (e.g. "https://peerterm.dev")
|
|
49
49
|
*/
|
|
50
|
-
export function printSessionBox({ code, expiry, mode, shell, startPath, shareUrl }) {
|
|
50
|
+
export function printSessionBox({ code, expiry, rejoinWindow, mode, shell, startPath, shareUrl }) {
|
|
51
51
|
const spacedCode = code.split('').join(' ');
|
|
52
52
|
const url = shareUrl || 'https://peerterm.dev';
|
|
53
53
|
|
|
@@ -66,6 +66,7 @@ export function printSessionBox({ code, expiry, mode, shell, startPath, shareUrl
|
|
|
66
66
|
const infoLines = [
|
|
67
67
|
` Session Code: ${spacedCode}`,
|
|
68
68
|
` Expires in: ${expiry}`,
|
|
69
|
+
` Rejoin Window: ${rejoinWindow}`,
|
|
69
70
|
` Mode: ${mode}`,
|
|
70
71
|
` Shell: ${shellDisplay}`,
|
|
71
72
|
` Path: ${pathDisplay}`,
|
|
@@ -98,6 +99,7 @@ Usage: peer-term [options]
|
|
|
98
99
|
|
|
99
100
|
Options:
|
|
100
101
|
--expiry <time> Session expiry time (e.g. 5m, 30s, 1h) [default: 5m]
|
|
102
|
+
--rejoin <time> Reconnection window (e.g. 5m, 1h, 6h) [default: 2m]
|
|
101
103
|
--readonly Share in view-only mode
|
|
102
104
|
--path <dir> Starting directory for the terminal session [default: home]
|
|
103
105
|
--relay <url> Custom relay server URL
|
|
@@ -108,6 +110,7 @@ Options:
|
|
|
108
110
|
Examples:
|
|
109
111
|
peer-term Start session with defaults
|
|
110
112
|
peer-term --expiry 10m Custom expiry
|
|
113
|
+
peer-term --rejoin 30m Custom reconnection window
|
|
111
114
|
peer-term --readonly View-only session
|
|
112
115
|
peer-term --path ~/projects Start in ~/projects
|
|
113
116
|
peer-term --verbose Debug logs
|