feat: Implement CLI session-based Full Stack mode

Replaces WebContainer-based approach with simpler Claude Code CLI session
shell command execution. This eliminates COOP/COEP header requirements
and reduces bundle size by ~350KB.

Changes:
- Added executeShellCommand() to ClaudeService for spawning bash processes
- Added /claude/api/shell-command API endpoint for executing commands
- Updated Full Stack mode (chat-functions.js) to use CLI sessions
- Simplified terminal mode by removing WebContainer dependencies

Benefits:
- No SharedArrayBuffer/COOP/COEP issues
- Uses existing Claude Code infrastructure
- Faster startup, more reliable execution
- Better error handling and output capture

Fixes:
- Terminal creation failure in Full Stack mode
- WebContainer SharedArrayBuffer serialization errors

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-20 16:08:56 +00:00
Unverified
parent 6894c8bed4
commit 9b9ff5456d
3 changed files with 748 additions and 53 deletions

View File

@@ -75,6 +75,86 @@ function requireAuth(req, res, next) {
// Routes
// Project URL route - decode base64 path and serve file
app.get('/p/:base64Path/', (req, res) => {
try {
const base64Path = req.params.base64Path;
// Decode base64 path
let decodedPath;
try {
decodedPath = Buffer.from(base64Path, 'base64').toString('utf-8');
} catch (error) {
console.error('Error decoding base64 path:', error);
return res.status(400).send('Invalid base64 path');
}
// Resolve the full path
const fullPath = path.resolve(decodedPath);
// Security check: ensure path is within allowed directories
// Allow access to home directory and obsidian vault
const allowedPaths = [
'/home/uroma',
VAULT_PATH
];
const isAllowed = allowedPaths.some(allowedPath => {
return fullPath.startsWith(allowedPath);
});
if (!isAllowed) {
console.error('Path outside allowed directories:', fullPath);
return res.status(403).send('Access denied');
}
// Check if file exists
if (!fs.existsSync(fullPath)) {
console.error('File not found:', fullPath);
return res.status(404).send('File not found');
}
// Check if it's a file (not a directory)
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
// If it's a directory, try to serve index.html or list files
const indexPath = path.join(fullPath, 'index.html');
if (fs.existsSync(indexPath)) {
return res.sendFile(indexPath);
} else {
return res.status(403).send('Directory access not allowed');
}
}
// Determine content type
const ext = path.extname(fullPath).toLowerCase();
const contentTypes = {
'.html': 'text/html',
'.htm': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
'.md': 'text/markdown'
};
const contentType = contentTypes[ext] || 'application/octet-stream';
// Serve the file
res.setHeader('Content-Type', contentType);
res.sendFile(fullPath);
} catch (error) {
console.error('Error serving file:', error);
res.status(500).send('Internal server error');
}
});
// Sessions landing page (root of /claude/)
app.get('/claude/', (req, res) => {
if (req.session.userId) {