diff --git a/server/index.js b/server/index.js index da6ca675..fa9ecc15 100755 --- a/server/index.js +++ b/server/index.js @@ -11,25 +11,7 @@ const __dirname = dirname(__filename); const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm'; -// ANSI color codes for terminal output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - cyan: '\x1b[36m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - dim: '\x1b[2m', -}; - -const c = { - info: (text) => `${colors.cyan}${text}${colors.reset}`, - ok: (text) => `${colors.green}${text}${colors.reset}`, - warn: (text) => `${colors.yellow}${text}${colors.reset}`, - tip: (text) => `${colors.blue}${text}${colors.reset}`, - bright: (text) => `${colors.bright}${text}${colors.reset}`, - dim: (text) => `${colors.dim}${text}${colors.reset}`, -}; +import { c } from './utils/colors.js'; console.log('SERVER_PORT from env:', process.env.SERVER_PORT); @@ -226,68 +208,7 @@ const server = http.createServer(app); const ptySessionsMap = new Map(); const PTY_SESSION_TIMEOUT = 30 * 60 * 1000; const SHELL_URL_PARSE_BUFFER_LIMIT = 32768; -const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g; -const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/; - -function stripAnsiSequences(value = '') { - return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, ''); -} - -function normalizeDetectedUrl(url) { - if (!url || typeof url !== 'string') return null; - - const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, ''); - if (!cleaned) return null; - - try { - const parsed = new URL(cleaned); - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - return null; - } - return parsed.toString(); - } catch { - return null; - } -} - -function extractUrlsFromText(value = '') { - const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || []; - - // Handle wrapped terminal URLs split across lines by terminal width. - const wrappedMatches = []; - const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/; - const lines = value.split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i); - if (!startMatch) continue; - - let combined = startMatch[0]; - let j = i + 1; - while (j < lines.length) { - const continuation = lines[j].trim(); - if (!continuation) break; - if (!continuationRegex.test(continuation)) break; - combined += continuation; - j++; - } - - wrappedMatches.push(combined.replace(/\r?\n\s*/g, '')); - } - - return Array.from(new Set([...directMatches, ...wrappedMatches])); -} - -function shouldAutoOpenUrlFromOutput(value = '') { - const normalized = value.toLowerCase(); - return ( - normalized.includes('browser didn\'t open') || - normalized.includes('open this url') || - normalized.includes('continue in your browser') || - normalized.includes('press enter to open') || - normalized.includes('open_url:') - ); -} +import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js'; // Single WebSocket server that handles both paths const wss = new WebSocketServer({ diff --git a/server/utils/colors.js b/server/utils/colors.js new file mode 100644 index 00000000..5dfa8a1d --- /dev/null +++ b/server/utils/colors.js @@ -0,0 +1,21 @@ +// ANSI color codes for terminal output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + dim: '\x1b[2m', +}; + +const c = { + info: (text) => `${colors.cyan}${text}${colors.reset}`, + ok: (text) => `${colors.green}${text}${colors.reset}`, + warn: (text) => `${colors.yellow}${text}${colors.reset}`, + tip: (text) => `${colors.blue}${text}${colors.reset}`, + bright: (text) => `${colors.bright}${text}${colors.reset}`, + dim: (text) => `${colors.dim}${text}${colors.reset}`, +}; + +export { colors, c }; diff --git a/server/utils/url-detection.js b/server/utils/url-detection.js new file mode 100644 index 00000000..428b306d --- /dev/null +++ b/server/utils/url-detection.js @@ -0,0 +1,71 @@ +const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g; +const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/; + +function stripAnsiSequences(value = '') { + return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, ''); +} + +function normalizeDetectedUrl(url) { + if (!url || typeof url !== 'string') return null; + + const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, ''); + if (!cleaned) return null; + + try { + const parsed = new URL(cleaned); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return null; + } + return parsed.toString(); + } catch { + return null; + } +} + +function extractUrlsFromText(value = '') { + const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || []; + + // Handle wrapped terminal URLs split across lines by terminal width. + const wrappedMatches = []; + const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/; + const lines = value.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i); + if (!startMatch) continue; + + let combined = startMatch[0]; + let j = i + 1; + while (j < lines.length) { + const continuation = lines[j].trim(); + if (!continuation) break; + if (!continuationRegex.test(continuation)) break; + combined += continuation; + j++; + } + + wrappedMatches.push(combined.replace(/\r?\n\s*/g, '')); + } + + return Array.from(new Set([...directMatches, ...wrappedMatches])); +} + +function shouldAutoOpenUrlFromOutput(value = '') { + const normalized = value.toLowerCase(); + return ( + normalized.includes('browser didn\'t open') || + normalized.includes('open this url') || + normalized.includes('continue in your browser') || + normalized.includes('press enter to open') || + normalized.includes('open_url:') + ); +} + +export { + ANSI_ESCAPE_SEQUENCE_REGEX, + TRAILING_URL_PUNCTUATION_REGEX, + stripAnsiSequences, + normalizeDetectedUrl, + extractUrlsFromText, + shouldAutoOpenUrlFromOutput +};