peer-term 1.3.9 → 1.4.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/bin/peer-term.js +1 -1
- package/package.json +1 -1
- package/src/cli.js +128 -0
- package/src/config.js +91 -0
- package/src/file-transfer.js +230 -0
- package/src/index.js +269 -1032
- package/src/prompts.js +143 -0
- package/src/ui.js +142 -142
- package/src/utils.js +113 -0
- package/src/viewer.js +259 -0
package/src/prompts.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — Interactive Prompts
|
|
3
|
+
*
|
|
4
|
+
* Terminal prompts for session expiry and rejoin window configuration.
|
|
5
|
+
* Handles three modes: CLI flag, interactive TTY prompt, silent default.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import readline from 'readline';
|
|
9
|
+
import { parseDuration } from './utils.js';
|
|
10
|
+
import { argv, expiryFlagPassed, rejoinFlagPassed } from './config.js';
|
|
11
|
+
import logger from './logger.js';
|
|
12
|
+
|
|
13
|
+
// ─── Generic Duration Prompt ─────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prompt the user for a duration value in the terminal.
|
|
17
|
+
* Re-prompts on invalid input until a valid value is entered or Enter is pressed for default.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} opts
|
|
20
|
+
* @param {string} opts.label - Main prompt question
|
|
21
|
+
* @param {string} opts.description - Additional context shown below the question
|
|
22
|
+
* @param {number} opts.defaultMs - Default value in milliseconds
|
|
23
|
+
* @param {string} opts.defaultLabel - Human-readable default (e.g. "5 minutes")
|
|
24
|
+
* @param {number} opts.minMs - Minimum allowed value in milliseconds
|
|
25
|
+
* @param {number} opts.maxMs - Maximum allowed value in milliseconds
|
|
26
|
+
* @param {string} opts.minLabel - Human-readable minimum (e.g. "2m")
|
|
27
|
+
* @param {string} opts.maxLabel - Human-readable maximum (e.g. "24h")
|
|
28
|
+
* @returns {Promise<number>} Resolved duration in milliseconds
|
|
29
|
+
*/
|
|
30
|
+
function promptDuration({ label, description, defaultMs, defaultLabel, minMs, maxMs, minLabel, maxLabel }) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const rl = readline.createInterface({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const ask = () => {
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(` ${label}`);
|
|
40
|
+
if (description) console.log(` ${description}`);
|
|
41
|
+
console.log(` Enter a value between ${minLabel} and ${maxLabel} (e.g. ${minLabel}, 30m, 2h)`);
|
|
42
|
+
console.log(` Default is ${defaultLabel}. Press Enter to use default.`);
|
|
43
|
+
rl.question(' > ', (answer) => {
|
|
44
|
+
const input = answer.trim();
|
|
45
|
+
|
|
46
|
+
// Empty input — use default
|
|
47
|
+
if (!input) {
|
|
48
|
+
rl.close();
|
|
49
|
+
resolve(defaultMs);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = parseDuration(input);
|
|
54
|
+
if (!parsed || parsed < minMs || parsed > maxMs) {
|
|
55
|
+
console.log(` Invalid value. Please enter a time between ${minLabel} and ${maxLabel} (e.g. 5m, 1h).`);
|
|
56
|
+
ask();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rl.close();
|
|
61
|
+
resolve(parsed);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
ask();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Expiry ──────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the session code expiry — from flag, interactive prompt, or silent default.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<number>} Expiry in milliseconds
|
|
75
|
+
*/
|
|
76
|
+
export async function getExpiryInteractive() {
|
|
77
|
+
const DEFAULT_EXPIRY = 5 * 60 * 1000; // 5 minutes
|
|
78
|
+
const MIN_EXPIRY = 2 * 60 * 1000; // 2 minutes
|
|
79
|
+
const MAX_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours
|
|
80
|
+
|
|
81
|
+
// If --expiry flag was explicitly passed, use it (validate and exit on error)
|
|
82
|
+
if (expiryFlagPassed) {
|
|
83
|
+
const parsed = parseDuration(argv.expiry);
|
|
84
|
+
if (!parsed || parsed < MIN_EXPIRY || parsed > MAX_EXPIRY) {
|
|
85
|
+
logger.error(`Invalid expiry: "${argv.expiry}". Must be between 2m and 24h.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Non-interactive — use default silently
|
|
92
|
+
if (!process.stdin.isTTY) return DEFAULT_EXPIRY;
|
|
93
|
+
|
|
94
|
+
// Interactive prompt
|
|
95
|
+
return promptDuration({
|
|
96
|
+
label: 'How long should the session code be valid?',
|
|
97
|
+
description: null,
|
|
98
|
+
defaultMs: DEFAULT_EXPIRY,
|
|
99
|
+
defaultLabel: '5 minutes',
|
|
100
|
+
minMs: MIN_EXPIRY,
|
|
101
|
+
maxMs: MAX_EXPIRY,
|
|
102
|
+
minLabel: '2m',
|
|
103
|
+
maxLabel: '24h',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Rejoin Window ───────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the reconnection window — from flag, interactive prompt, or silent default.
|
|
111
|
+
*
|
|
112
|
+
* @returns {Promise<number>} Rejoin window in milliseconds
|
|
113
|
+
*/
|
|
114
|
+
export async function getRejoinInteractive() {
|
|
115
|
+
const DEFAULT_REJOIN = 5 * 60 * 1000; // 5 minutes
|
|
116
|
+
const MIN_REJOIN = 5 * 60 * 1000; // 5 minutes
|
|
117
|
+
const MAX_REJOIN = 6 * 60 * 60 * 1000; // 6 hours
|
|
118
|
+
|
|
119
|
+
// If --rejoin flag was explicitly passed, use it
|
|
120
|
+
if (rejoinFlagPassed) {
|
|
121
|
+
const parsed = parseDuration(argv.rejoin);
|
|
122
|
+
if (!parsed || parsed < MIN_REJOIN || parsed > MAX_REJOIN) {
|
|
123
|
+
logger.error(`Invalid rejoin window: "${argv.rejoin}". Must be between 5m and 6h.`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Non-interactive — use default silently
|
|
130
|
+
if (!process.stdin.isTTY) return DEFAULT_REJOIN;
|
|
131
|
+
|
|
132
|
+
// Interactive prompt
|
|
133
|
+
return promptDuration({
|
|
134
|
+
label: 'How long should the reconnection window be?',
|
|
135
|
+
description: 'This is how long a disconnected client or host has to rejoin before the session ends.',
|
|
136
|
+
defaultMs: DEFAULT_REJOIN,
|
|
137
|
+
defaultLabel: '2 minutes',
|
|
138
|
+
minMs: MIN_REJOIN,
|
|
139
|
+
maxMs: MAX_REJOIN,
|
|
140
|
+
minLabel: '5m',
|
|
141
|
+
maxLabel: '6h',
|
|
142
|
+
});
|
|
143
|
+
}
|
package/src/ui.js
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PeerTerm — CLI UI Module
|
|
3
|
-
*
|
|
4
|
-
* Handles all terminal output formatting:
|
|
5
|
-
* - ASCII art banner
|
|
6
|
-
* - Session code display box
|
|
7
|
-
* - Help text
|
|
8
|
-
* - Version display
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'fs';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(__filename);
|
|
17
|
-
|
|
18
|
-
// ─── ASCII Banner ────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const BANNER = `
|
|
21
|
-
██████╗ ███████╗███████╗██████╗ ████████╗███████╗██████╗ ███╗ ███╗
|
|
22
|
-
██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔══██╗████╗ ████║
|
|
23
|
-
██████╔╝█████╗ █████╗ ██████╔╝ ██║ █████╗ ██████╔╝██╔████╔██║
|
|
24
|
-
██╔═══╝ ██╔══╝ ██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║
|
|
25
|
-
██║ ███████╗███████╗██║ ██║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║
|
|
26
|
-
╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
27
|
-
Terminal sharing. Instant. Encrypted.`;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Print the startup ASCII art banner.
|
|
31
|
-
*/
|
|
32
|
-
export function printBanner() {
|
|
33
|
-
console.log(BANNER);
|
|
34
|
-
console.log('');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ─── Session Code Box ────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Print the bordered session information box.
|
|
41
|
-
*
|
|
42
|
-
* @param {Object} opts
|
|
43
|
-
* @param {string} opts.code - 6-digit session code
|
|
44
|
-
* @param {string} opts.expiry - Formatted expiry string (e.g. "5 minute(s)")
|
|
45
|
-
* @param {string} opts.mode - "Read-Write" or "Read-Only"
|
|
46
|
-
* @param {string} opts.shell - Shell path (e.g. "/bin/zsh")
|
|
47
|
-
* @param {string} [opts.startPath] - Starting directory for the session
|
|
48
|
-
* @param {string} [opts.shareUrl] - URL to share (e.g. "https://peerterm.dev")
|
|
49
|
-
*/
|
|
50
|
-
export function printSessionBox({ code, expiry, rejoinWindow, mode, shell, startPath, shareUrl }) {
|
|
51
|
-
const spacedCode = code.split('').join(' ');
|
|
52
|
-
const url = shareUrl || 'https://peerterm.dev';
|
|
53
|
-
|
|
54
|
-
// Truncate long shell paths to keep box readable
|
|
55
|
-
const shellDisplay = shell.length > 24 ? '...' + shell.slice(-21) : shell;
|
|
56
|
-
|
|
57
|
-
// Truncate long start paths similarly
|
|
58
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
59
|
-
let pathDisplay = startPath || homeDir;
|
|
60
|
-
if (homeDir && pathDisplay.startsWith(homeDir)) {
|
|
61
|
-
pathDisplay = '~' + pathDisplay.slice(homeDir.length);
|
|
62
|
-
}
|
|
63
|
-
if (pathDisplay.length > 30) pathDisplay = '...' + pathDisplay.slice(-27);
|
|
64
|
-
|
|
65
|
-
// Calculate dynamic box width based on longest content
|
|
66
|
-
const infoLines = [
|
|
67
|
-
` Session Code: ${spacedCode}`,
|
|
68
|
-
` Expires in: ${expiry}`,
|
|
69
|
-
` Rejoin Window: ${rejoinWindow}`,
|
|
70
|
-
` Mode: ${mode}`,
|
|
71
|
-
` Shell: ${shellDisplay}`,
|
|
72
|
-
` Path: ${pathDisplay}`,
|
|
73
|
-
];
|
|
74
|
-
const shareLine = ` Share at: ${url}`;
|
|
75
|
-
const allLines = [...infoLines, shareLine];
|
|
76
|
-
const maxLen = Math.max(...allLines.map(l => l.length), 35);
|
|
77
|
-
const innerWidth = maxLen + 3; // padding on right
|
|
78
|
-
|
|
79
|
-
const hr = '─'.repeat(innerWidth);
|
|
80
|
-
const emptyLine = ' '.repeat(innerWidth);
|
|
81
|
-
|
|
82
|
-
console.log('');
|
|
83
|
-
console.log(` ┌${hr}┐`);
|
|
84
|
-
console.log(` │${emptyLine}│`);
|
|
85
|
-
for (const line of infoLines) {
|
|
86
|
-
console.log(` │${line.padEnd(innerWidth)}│`);
|
|
87
|
-
}
|
|
88
|
-
console.log(` │${emptyLine}│`);
|
|
89
|
-
console.log(` │${shareLine.padEnd(innerWidth)}│`);
|
|
90
|
-
console.log(` │${emptyLine}│`);
|
|
91
|
-
console.log(` └${hr}┘`);
|
|
92
|
-
console.log('');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── Help Text ───────────────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
const HELP_TEXT = `
|
|
98
|
-
Usage: peer-term [options]
|
|
99
|
-
|
|
100
|
-
Options:
|
|
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]
|
|
103
|
-
--readonly Share in view-only mode
|
|
104
|
-
--secure Enable fingerprint verification for MITM protection
|
|
105
|
-
--path <dir> Starting directory for the terminal session [default: home]
|
|
106
|
-
--relay <url> Custom relay server URL
|
|
107
|
-
--verbose Enable debug logging
|
|
108
|
-
--help Show this help message
|
|
109
|
-
--version Print version number
|
|
110
|
-
|
|
111
|
-
Examples:
|
|
112
|
-
peer-term Start session with defaults
|
|
113
|
-
peer-term --expiry 10m Custom expiry
|
|
114
|
-
peer-term --rejoin 30m Custom reconnection window
|
|
115
|
-
peer-term --readonly View-only session
|
|
116
|
-
peer-term --secure Require fingerprint verification
|
|
117
|
-
peer-term --path ~/projects Start in ~/projects
|
|
118
|
-
peer-term --verbose Debug logs
|
|
119
|
-
peer-term --relay wss://custom Use custom relay server
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Print CLI help text.
|
|
124
|
-
*/
|
|
125
|
-
export function printHelp() {
|
|
126
|
-
console.log(HELP_TEXT);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── Version ─────────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Print the version number from package.json.
|
|
133
|
-
*/
|
|
134
|
-
export function printVersion() {
|
|
135
|
-
try {
|
|
136
|
-
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
137
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
138
|
-
console.log(`peer-term v${pkg.version}`);
|
|
139
|
-
} catch {
|
|
140
|
-
console.log('peer-term (version unknown)');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — CLI UI Module
|
|
3
|
+
*
|
|
4
|
+
* Handles all terminal output formatting:
|
|
5
|
+
* - ASCII art banner
|
|
6
|
+
* - Session code display box
|
|
7
|
+
* - Help text
|
|
8
|
+
* - Version display
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// ─── ASCII Banner ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const BANNER = `
|
|
21
|
+
██████╗ ███████╗███████╗██████╗ ████████╗███████╗██████╗ ███╗ ███╗
|
|
22
|
+
██╔══██╗██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔══██╗████╗ ████║
|
|
23
|
+
██████╔╝█████╗ █████╗ ██████╔╝ ██║ █████╗ ██████╔╝██╔████╔██║
|
|
24
|
+
██╔═══╝ ██╔══╝ ██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║
|
|
25
|
+
██║ ███████╗███████╗██║ ██║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║
|
|
26
|
+
╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
27
|
+
Terminal sharing. Instant. Encrypted.`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Print the startup ASCII art banner.
|
|
31
|
+
*/
|
|
32
|
+
export function printBanner() {
|
|
33
|
+
console.log(BANNER);
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Session Code Box ────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Print the bordered session information box.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} opts
|
|
43
|
+
* @param {string} opts.code - 6-digit session code
|
|
44
|
+
* @param {string} opts.expiry - Formatted expiry string (e.g. "5 minute(s)")
|
|
45
|
+
* @param {string} opts.mode - "Read-Write" or "Read-Only"
|
|
46
|
+
* @param {string} opts.shell - Shell path (e.g. "/bin/zsh")
|
|
47
|
+
* @param {string} [opts.startPath] - Starting directory for the session
|
|
48
|
+
* @param {string} [opts.shareUrl] - URL to share (e.g. "https://peerterm.dev")
|
|
49
|
+
*/
|
|
50
|
+
export function printSessionBox({ code, expiry, rejoinWindow, mode, shell, startPath, shareUrl }) {
|
|
51
|
+
const spacedCode = code.split('').join(' ');
|
|
52
|
+
const url = shareUrl || 'https://peerterm.dev';
|
|
53
|
+
|
|
54
|
+
// Truncate long shell paths to keep box readable
|
|
55
|
+
const shellDisplay = shell.length > 24 ? '...' + shell.slice(-21) : shell;
|
|
56
|
+
|
|
57
|
+
// Truncate long start paths similarly
|
|
58
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
59
|
+
let pathDisplay = startPath || homeDir;
|
|
60
|
+
if (homeDir && pathDisplay.startsWith(homeDir)) {
|
|
61
|
+
pathDisplay = '~' + pathDisplay.slice(homeDir.length);
|
|
62
|
+
}
|
|
63
|
+
if (pathDisplay.length > 30) pathDisplay = '...' + pathDisplay.slice(-27);
|
|
64
|
+
|
|
65
|
+
// Calculate dynamic box width based on longest content
|
|
66
|
+
const infoLines = [
|
|
67
|
+
` Session Code: ${spacedCode}`,
|
|
68
|
+
` Expires in: ${expiry}`,
|
|
69
|
+
` Rejoin Window: ${rejoinWindow}`,
|
|
70
|
+
` Mode: ${mode}`,
|
|
71
|
+
` Shell: ${shellDisplay}`,
|
|
72
|
+
` Path: ${pathDisplay}`,
|
|
73
|
+
];
|
|
74
|
+
const shareLine = ` Share at: ${url}`;
|
|
75
|
+
const allLines = [...infoLines, shareLine];
|
|
76
|
+
const maxLen = Math.max(...allLines.map(l => l.length), 35);
|
|
77
|
+
const innerWidth = maxLen + 3; // padding on right
|
|
78
|
+
|
|
79
|
+
const hr = '─'.repeat(innerWidth);
|
|
80
|
+
const emptyLine = ' '.repeat(innerWidth);
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(` ┌${hr}┐`);
|
|
84
|
+
console.log(` │${emptyLine}│`);
|
|
85
|
+
for (const line of infoLines) {
|
|
86
|
+
console.log(` │${line.padEnd(innerWidth)}│`);
|
|
87
|
+
}
|
|
88
|
+
console.log(` │${emptyLine}│`);
|
|
89
|
+
console.log(` │${shareLine.padEnd(innerWidth)}│`);
|
|
90
|
+
console.log(` │${emptyLine}│`);
|
|
91
|
+
console.log(` └${hr}┘`);
|
|
92
|
+
console.log('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Help Text ───────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
const HELP_TEXT = `
|
|
98
|
+
Usage: peer-term [options]
|
|
99
|
+
|
|
100
|
+
Options:
|
|
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]
|
|
103
|
+
--readonly Share in view-only mode
|
|
104
|
+
--secure Enable fingerprint verification for MITM protection
|
|
105
|
+
--path <dir> Starting directory for the terminal session [default: home]
|
|
106
|
+
--relay <url> Custom relay server URL
|
|
107
|
+
--verbose Enable debug logging
|
|
108
|
+
--help Show this help message
|
|
109
|
+
--version Print version number
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
peer-term Start session with defaults
|
|
113
|
+
peer-term --expiry 10m Custom expiry
|
|
114
|
+
peer-term --rejoin 30m Custom reconnection window
|
|
115
|
+
peer-term --readonly View-only session
|
|
116
|
+
peer-term --secure Require fingerprint verification
|
|
117
|
+
peer-term --path ~/projects Start in ~/projects
|
|
118
|
+
peer-term --verbose Debug logs
|
|
119
|
+
peer-term --relay wss://custom Use custom relay server
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Print CLI help text.
|
|
124
|
+
*/
|
|
125
|
+
export function printHelp() {
|
|
126
|
+
console.log(HELP_TEXT);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Version ─────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print the version number from package.json.
|
|
133
|
+
*/
|
|
134
|
+
export function printVersion() {
|
|
135
|
+
try {
|
|
136
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
137
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
138
|
+
console.log(`peer-term v${pkg.version}`);
|
|
139
|
+
} catch {
|
|
140
|
+
console.log('peer-term (version unknown)');
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerTerm — Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers with no domain dependencies:
|
|
5
|
+
* - Duration parsing and formatting
|
|
6
|
+
* - Path resolution (tilde expansion, start path validation)
|
|
7
|
+
* - Shell detection
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import logger from './logger.js';
|
|
14
|
+
|
|
15
|
+
// ─── Duration Parsing ────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a human-readable duration string into milliseconds.
|
|
19
|
+
* Accepted formats: "30s", "5m", "1h" (case-insensitive).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} str - Duration string
|
|
22
|
+
* @returns {number|null} Duration in milliseconds, or null if invalid
|
|
23
|
+
*/
|
|
24
|
+
export function parseDuration(str) {
|
|
25
|
+
const match = str.match(/^(\d+)(s|m|h)$/i);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
const value = parseInt(match[1], 10);
|
|
28
|
+
const unit = match[2].toLowerCase();
|
|
29
|
+
switch (unit) {
|
|
30
|
+
case 's': return value * 1000;
|
|
31
|
+
case 'm': return value * 60 * 1000;
|
|
32
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
33
|
+
default: return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format a duration in milliseconds to a human-readable string.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} ms - Duration in milliseconds
|
|
41
|
+
* @returns {string} Formatted string (e.g. "5 minute(s)")
|
|
42
|
+
*/
|
|
43
|
+
export function formatDuration(ms) {
|
|
44
|
+
if (ms >= 3600000) return `${Math.round(ms / 3600000)} hour(s)`;
|
|
45
|
+
if (ms >= 60000) return `${Math.round(ms / 60000)} minute(s)`;
|
|
46
|
+
return `${Math.round(ms / 1000)} second(s)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Path Resolution ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Expand a leading ~ to the user's home directory.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} inputPath
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function expandTilde(inputPath) {
|
|
58
|
+
if (inputPath.startsWith('~/') || inputPath === '~') {
|
|
59
|
+
return inputPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '.');
|
|
60
|
+
}
|
|
61
|
+
return inputPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve and validate a starting directory path.
|
|
66
|
+
* Falls back to the user's home directory if no path is provided.
|
|
67
|
+
* Exits the process if the path doesn't exist or isn't a directory.
|
|
68
|
+
*
|
|
69
|
+
* @param {string|undefined} inputPath
|
|
70
|
+
* @returns {string} Resolved absolute path
|
|
71
|
+
*/
|
|
72
|
+
export function resolveStartPath(inputPath) {
|
|
73
|
+
if (!inputPath) return process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
74
|
+
|
|
75
|
+
const resolved = path.resolve(expandTilde(inputPath));
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(resolved)) {
|
|
78
|
+
logger.error(`Path does not exist: ${resolved}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const stat = fs.statSync(resolved);
|
|
83
|
+
if (!stat.isDirectory()) {
|
|
84
|
+
logger.error(`Path is not a directory: ${resolved}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Shell Detection ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect the user's default shell.
|
|
95
|
+
* Checks $SHELL, Windows COMSPEC, /etc/passwd, and falls back to bash/powershell.
|
|
96
|
+
*
|
|
97
|
+
* @returns {string} Shell executable path or name
|
|
98
|
+
*/
|
|
99
|
+
export function detectShell() {
|
|
100
|
+
const platform = os.platform();
|
|
101
|
+
if (process.env.SHELL) return process.env.SHELL;
|
|
102
|
+
if (platform === 'win32') return process.env.COMSPEC || 'powershell.exe';
|
|
103
|
+
try {
|
|
104
|
+
const passwd = fs.readFileSync('/etc/passwd', 'utf-8');
|
|
105
|
+
const username = os.userInfo().username;
|
|
106
|
+
const line = passwd.split('\n').find((l) => l.startsWith(username + ':'));
|
|
107
|
+
if (line) {
|
|
108
|
+
const shell = line.split(':').pop().trim();
|
|
109
|
+
if (shell) return shell;
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
return platform === 'win32' ? 'powershell.exe' : 'bash';
|
|
113
|
+
}
|