[web] Added Web REPL server and frontend demo
This commit is contained in:
parent
068da2fb41
commit
e6b88c9dec
|
@ -0,0 +1,240 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nasal Interpreter Web REPL</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #2c3e50;
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-container {
|
||||||
|
background: #34495e;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-output {
|
||||||
|
flex-grow: 1;
|
||||||
|
background: #21252b;
|
||||||
|
color: #abb2bf;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-prompt {
|
||||||
|
color: #3498db;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
padding: 5px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repl-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
background: #21252b;
|
||||||
|
border: none;
|
||||||
|
color: #abb2bf;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
min-height: 24px;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-btn {
|
||||||
|
background-color: #27ae60;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-btn:hover {
|
||||||
|
background-color: #219a52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line {
|
||||||
|
margin: 2px 0;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-line {
|
||||||
|
color: #3498db;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-line {
|
||||||
|
color: #e74c3c;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-line {
|
||||||
|
color: #2ecc71;
|
||||||
|
white-space: pre;
|
||||||
|
margin-left: 4px; /* Slight indent for results */
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-message {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
color: #95a5a6;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-type {
|
||||||
|
color: #ff5f5f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: #abb2bf;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-arrow, .error-line-number {
|
||||||
|
color: #56b6c2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-file {
|
||||||
|
color: #ff5f5f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
color: #abb2bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-pointer-space {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-pointer {
|
||||||
|
color: #ff5f5f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-red-bold {
|
||||||
|
color: #e06c75;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-cyan-bold {
|
||||||
|
color: #56b6c2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.repl-container {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Nasal Interpreter Web REPL</h1>
|
||||||
|
<p>Interactive Read-Eval-Print Loop for Nasal</p>
|
||||||
|
<p>Powered by ValKmjolnir's <a href="https://www.fgprc.org.cn/nasal_interpreter.html">Nasal Interpreter</a>, Web App by <a href="https://sidi762.github.io">LIANG Sidi</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="repl-container">
|
||||||
|
<div id="repl-output" class="repl-output">
|
||||||
|
<div class="output-line">Welcome to Nasal Web REPL Demo!</div>
|
||||||
|
</div>
|
||||||
|
<div class="repl-input-container">
|
||||||
|
<span class="repl-prompt">>>></span>
|
||||||
|
<textarea id="repl-input" class="repl-input" rows="1"
|
||||||
|
placeholder="Enter Nasal code here..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="clearREPL()">Clear REPL</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="examples">
|
||||||
|
<h3>Example Commands:</h3>
|
||||||
|
<button class="example-btn" onclick="insertExample('basic')">Basic Math</button>
|
||||||
|
<button class="example-btn" onclick="insertExample('loops')">Loops</button>
|
||||||
|
<button class="example-btn" onclick="insertExample('functions')">Functions</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="repl.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,249 @@
|
||||||
|
let replSessionId = null;
|
||||||
|
let multilineInput = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
let commandHistory = [];
|
||||||
|
let multilineBuffer = [];
|
||||||
|
let isMultilineMode = false;
|
||||||
|
|
||||||
|
// Initialize REPL
|
||||||
|
async function initRepl() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/repl/init', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.sessionId) {
|
||||||
|
replSessionId = data.sessionId;
|
||||||
|
console.log('REPL session initialized:', replSessionId);
|
||||||
|
|
||||||
|
// Display version info
|
||||||
|
appendOutput('Nasal REPL interpreter version ' + data.version);
|
||||||
|
appendOutput('Type your code below and press Enter to execute.');
|
||||||
|
appendOutput('Use Shift+Enter for multiline input.\n');
|
||||||
|
showPrompt();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to initialize REPL session');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
appendOutput(`Error: ${err.message}`, 'error-line');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format error messages to match command-line REPL
|
||||||
|
function formatError(error) {
|
||||||
|
// Split the error message into lines
|
||||||
|
const lines = error.split('\n');
|
||||||
|
return lines.map(line => {
|
||||||
|
// Add appropriate indentation for the error pointer
|
||||||
|
if (line.includes('-->')) {
|
||||||
|
return ' ' + line;
|
||||||
|
} else if (line.includes('^')) {
|
||||||
|
return ' ' + line;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input
|
||||||
|
const input = document.getElementById('repl-input');
|
||||||
|
input.addEventListener('keydown', async (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
// Shift+Enter: add newline
|
||||||
|
const pos = input.selectionStart;
|
||||||
|
const value = input.value;
|
||||||
|
input.value = value.substring(0, pos) + '\n' + value.substring(pos);
|
||||||
|
input.selectionStart = input.selectionEnd = pos + 1;
|
||||||
|
input.style.height = 'auto';
|
||||||
|
input.style.height = input.scrollHeight + 'px';
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const code = input.value.trim();
|
||||||
|
|
||||||
|
// Skip empty lines but still show prompt
|
||||||
|
if (!code) {
|
||||||
|
showPrompt(isMultilineMode ? '... ' : '>>> ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First check if input is complete
|
||||||
|
const checkResponse = await fetch('/repl/eval', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: replSessionId,
|
||||||
|
line: code,
|
||||||
|
check: true,
|
||||||
|
buffer: multilineBuffer // Send existing buffer
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkData = await checkResponse.json();
|
||||||
|
|
||||||
|
if (checkData.needsMore) {
|
||||||
|
// Add to multiline buffer and show continuation prompt
|
||||||
|
multilineBuffer.push(code);
|
||||||
|
isMultilineMode = true;
|
||||||
|
|
||||||
|
// Display the input with continuation prompt
|
||||||
|
appendOutput(code, 'input-line', multilineBuffer.length === 1 ? '>>> ' : '... ');
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
showPrompt('... ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were in multiline mode, add the final line
|
||||||
|
if (isMultilineMode) {
|
||||||
|
multilineBuffer.push(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the complete code to evaluate
|
||||||
|
const fullCode = isMultilineMode ?
|
||||||
|
multilineBuffer.join('\n') : code;
|
||||||
|
|
||||||
|
// Display the input
|
||||||
|
appendOutput(code, 'input-line', isMultilineMode ? '... ' : '>>> ');
|
||||||
|
|
||||||
|
// Evaluate the code
|
||||||
|
const response = await fetch('/repl/eval', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: replSessionId,
|
||||||
|
line: fullCode
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
appendOutput(formatError(data.error.trim()), 'error-line');
|
||||||
|
} else if (data.result) {
|
||||||
|
handleResult(data.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset multiline state
|
||||||
|
multilineBuffer = [];
|
||||||
|
isMultilineMode = false;
|
||||||
|
input.value = '';
|
||||||
|
showPrompt('>>> ');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
appendOutput(`Error: ${err.message}`, 'error-line');
|
||||||
|
multilineBuffer = [];
|
||||||
|
isMultilineMode = false;
|
||||||
|
input.value = '';
|
||||||
|
showPrompt('>>> ');
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (historyIndex < commandHistory.length - 1) {
|
||||||
|
historyIndex++;
|
||||||
|
input.value = commandHistory[historyIndex];
|
||||||
|
} else {
|
||||||
|
historyIndex = commandHistory.length;
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-resize input
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
input.style.height = 'auto';
|
||||||
|
input.style.height = input.scrollHeight + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show prompt and scroll to bottom
|
||||||
|
function showPrompt(prompt = '>>> ') {
|
||||||
|
const promptSpan = document.querySelector('.repl-prompt');
|
||||||
|
if (promptSpan) {
|
||||||
|
promptSpan.textContent = prompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append output to REPL
|
||||||
|
function appendOutput(text, className = '', prefix = '') {
|
||||||
|
const output = document.getElementById('repl-output');
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = `output-line ${className}`;
|
||||||
|
|
||||||
|
line.innerHTML = prefix + formatErrorMessage(text);
|
||||||
|
|
||||||
|
|
||||||
|
output.appendChild(line);
|
||||||
|
output.scrollTop = output.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear REPL
|
||||||
|
function clearREPL() {
|
||||||
|
const output = document.getElementById('repl-output');
|
||||||
|
output.innerHTML = '';
|
||||||
|
appendOutput('Screen cleared', 'system-message');
|
||||||
|
showPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example snippets
|
||||||
|
const examples = {
|
||||||
|
basic: `var x = 1011 + 1013;
|
||||||
|
println("x = ", x);`,
|
||||||
|
|
||||||
|
loops: `var sum = 0;
|
||||||
|
for(var i = 1; i <= 5; i += 1) {
|
||||||
|
sum += i;
|
||||||
|
}
|
||||||
|
println("Sum:", sum);`,
|
||||||
|
|
||||||
|
functions: `var factorial = func(n) {
|
||||||
|
if (n <= 1) return 1;
|
||||||
|
return n * factorial(n - 1);
|
||||||
|
};
|
||||||
|
factorial(5);`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert example into input
|
||||||
|
function insertExample(type) {
|
||||||
|
input.value = examples[type];
|
||||||
|
input.style.height = 'auto';
|
||||||
|
input.style.height = input.scrollHeight + 'px';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize REPL on page load
|
||||||
|
window.addEventListener('load', initRepl);
|
||||||
|
|
||||||
|
// Add these utility functions
|
||||||
|
function formatErrorMessage(text) {
|
||||||
|
// Replace ANSI escape codes with CSS classes
|
||||||
|
return text
|
||||||
|
// Remove any existing formatting first
|
||||||
|
.replace(/\u001b\[\d+(?:;\d+)*m/g, '')
|
||||||
|
// Format the error line
|
||||||
|
.replace(/^parse: (.+)$/m, '<span class="error-type">parse:</span> <span class="error-desc">$1</span>')
|
||||||
|
// Format the file location
|
||||||
|
.replace(/^\s*--> (.+?)(\d+):(\d+)$/m, '<span class="error-arrow">--></span> <span class="error-file">$1</span><span class="error-loc">$2:$3</span>')
|
||||||
|
// Format the code line
|
||||||
|
.replace(/^(\d+ \|)(.*)$/m, '<span class="error-line-number">$1</span><span class="error-code">$2</span>')
|
||||||
|
// Format the error pointer
|
||||||
|
.replace(/^(\s*\|)(\s*)(\^+)$/m, '<span class="error-line-number">$1</span><span class="error-pointer-space">$2</span><span class="error-pointer">$3</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResult(result) {
|
||||||
|
const lines = result.split('\n');
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (line.trim()) {
|
||||||
|
appendOutput(line.trim(), 'result-line');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
const express = require('express');
|
||||||
|
const ffi = require('ffi-napi');
|
||||||
|
const path = require('path');
|
||||||
|
const yargs = require('yargs/yargs');
|
||||||
|
const { hideBin } = require('yargs/helpers');
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const argv = yargs(hideBin(process.argv))
|
||||||
|
.usage('Usage: $0 [options]')
|
||||||
|
.option('verbose', {
|
||||||
|
alias: 'v',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Run with verbose logging'
|
||||||
|
})
|
||||||
|
.option('port', {
|
||||||
|
alias: 'p',
|
||||||
|
type: 'number',
|
||||||
|
description: 'Port to run the server on',
|
||||||
|
default: 3001
|
||||||
|
})
|
||||||
|
.option('host', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Host to run the server on',
|
||||||
|
default: 'localhost'
|
||||||
|
})
|
||||||
|
.help()
|
||||||
|
.alias('help', 'h')
|
||||||
|
.version()
|
||||||
|
.argv;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
|
||||||
|
// Load Nasal REPL library functions
|
||||||
|
const nasalLib = ffi.Library(path.join(__dirname, '../module/libnasal-web'), {
|
||||||
|
'nasal_repl_init': ['pointer', []],
|
||||||
|
'nasal_repl_cleanup': ['void', ['pointer']],
|
||||||
|
'nasal_repl_eval': ['string', ['pointer', 'string']],
|
||||||
|
'nasal_repl_is_complete': ['int', ['pointer', 'string']],
|
||||||
|
'nasal_repl_get_version': ['string', []],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store active REPL sessions
|
||||||
|
const replSessions = new Map();
|
||||||
|
|
||||||
|
// Clean up inactive sessions periodically (30 minutes timeout)
|
||||||
|
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [sessionId, session] of replSessions.entries()) {
|
||||||
|
if (now - session.lastAccess > SESSION_TIMEOUT) {
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(`Cleaning up inactive session: ${sessionId}`);
|
||||||
|
}
|
||||||
|
nasalLib.nasal_repl_cleanup(session.context);
|
||||||
|
replSessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 60000); // Check every minute
|
||||||
|
|
||||||
|
app.post('/repl/init', (req, res) => {
|
||||||
|
try {
|
||||||
|
const ctx = nasalLib.nasal_repl_init();
|
||||||
|
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const version = nasalLib.nasal_repl_get_version();
|
||||||
|
|
||||||
|
replSessions.set(sessionId, {
|
||||||
|
context: ctx,
|
||||||
|
lastAccess: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(`New REPL session initialized: ${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
sessionId,
|
||||||
|
version
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.error('Failed to initialize REPL session:', err);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: 'Failed to initialize REPL session' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/repl/eval', (req, res) => {
|
||||||
|
const { sessionId, line, check, buffer } = req.body;
|
||||||
|
|
||||||
|
if (!sessionId || !replSessions.has(sessionId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid or expired session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line) {
|
||||||
|
return res.status(400).json({ error: 'No code provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = replSessions.get(sessionId);
|
||||||
|
session.lastAccess = Date.now();
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
const codeToCheck = buffer ? [...buffer, line].join('\n') : line;
|
||||||
|
const isComplete = nasalLib.nasal_repl_is_complete(session.context, codeToCheck);
|
||||||
|
|
||||||
|
if (isComplete === 1) {
|
||||||
|
return res.json({ needsMore: true });
|
||||||
|
} else if (isComplete === -1) {
|
||||||
|
return res.json({ error: 'Invalid input' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = nasalLib.nasal_repl_eval(session.context, line);
|
||||||
|
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(`REPL evaluation for session ${sessionId}:`, { line, result });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ result });
|
||||||
|
} catch (err) {
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.error(`REPL evaluation error for session ${sessionId}:`, err);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/repl/cleanup', (req, res) => {
|
||||||
|
const { sessionId } = req.body;
|
||||||
|
|
||||||
|
if (!sessionId || !replSessions.has(sessionId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid session' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = replSessions.get(sessionId);
|
||||||
|
nasalLib.nasal_repl_cleanup(session.context);
|
||||||
|
replSessions.delete(sessionId);
|
||||||
|
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(`REPL session cleaned up: ${sessionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Session cleaned up successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.error(`Failed to cleanup session ${sessionId}:`, err);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle cleanup on server shutdown
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\nCleaning up REPL sessions before exit...');
|
||||||
|
for (const [sessionId, session] of replSessions.entries()) {
|
||||||
|
try {
|
||||||
|
nasalLib.nasal_repl_cleanup(session.context);
|
||||||
|
if (argv.verbose) {
|
||||||
|
console.log(`Cleaned up session: ${sessionId}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error cleaning up session ${sessionId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = argv.port || 3001;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`REPL Server running on http://localhost:${PORT}`);
|
||||||
|
if (argv.verbose) console.log('Verbose logging enabled');
|
||||||
|
});
|
Loading…
Reference in New Issue