peer-term 1.1.8 → 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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +153 -14
  3. package/src/ui.js +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peer-term",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Share your terminal instantly using a 6-digit code. Encrypted. No config.",
6
6
  "keywords": [
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.1.8',
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('Client disconnected. Rejoin window: 2 minutes.');
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('Client disconnected. Rejoin window: 2 minutes.');
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
- logger.info(`Shell: ${shell}`);
1026
- logger.info(`Relays: ${RELAY_URLS.join(', ')}`);
1027
- logger.info(`Path: ${startPath}`);
1028
- if (readOnly) logger.info('Mode: READ-ONLY');
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