diff --git a/apps/backend/bun.lock b/apps/backend/bun.lock
index 167e10a..9773556 100644
--- a/apps/backend/bun.lock
+++ b/apps/backend/bun.lock
@@ -21,7 +21,7 @@
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
- "typescript": "5.8.3",
+ "typescript": "^5.9.2",
"typescript-eslint": "^8.30.0",
},
"peerDependencies": {
diff --git a/apps/backend/index.ts b/apps/backend/index.ts
new file mode 100644
index 0000000..f67b2c6
--- /dev/null
+++ b/apps/backend/index.ts
@@ -0,0 +1 @@
+console.log("Hello via Bun!");
\ No newline at end of file
diff --git a/apps/backend/package.json b/apps/backend/package.json
index 609b209..ec357f1 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -22,7 +22,7 @@
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
- "typescript": "5.8.3",
+ "typescript": "^5.8.3",
"typescript-eslint": "^8.30.0"
},
"private": true,
@@ -30,7 +30,10 @@
"dev": "bun run --watch src/index.ts",
"build": "bun build src/index.ts --outdir ./dist --target node",
"lint": "bun eslint src/**/*.ts",
- "format": "bun prettier --write 'src/**/*.ts'"
+ "format": "bun prettier --write 'src/**/*.ts'",
+ "setup": "bun run scripts/setup.ts",
+ "clean": "bun run scripts/clean.ts",
+ "command-executor": "bun run scripts/command-executor.ts"
},
"type": "module"
}
diff --git a/apps/backend/scripts/clean.ts b/apps/backend/scripts/clean.ts
new file mode 100644
index 0000000..7e8a815
--- /dev/null
+++ b/apps/backend/scripts/clean.ts
@@ -0,0 +1,15 @@
+import { fileManager } from '../src/utils/fileManager';
+
+async function main() {
+ const customPath = process.argv[2];
+
+ try {
+ await fileManager.cleanupProject(customPath);
+ console.log(' Cleanup completed successfully');
+ } catch (error) {
+ console.error(' Cleanup failed:', error instanceof Error ? error.message : error);
+ process.exit(1);
+ }
+}
+
+await main();
\ No newline at end of file
diff --git a/apps/backend/scripts/command-executor.ts b/apps/backend/scripts/command-executor.ts
new file mode 100644
index 0000000..d947845
--- /dev/null
+++ b/apps/backend/scripts/command-executor.ts
@@ -0,0 +1,58 @@
+import { executeCommand } from "../src/utils/commandExecutor";
+
+interface CommandError extends Error {
+ stderr?: string;
+ stdout?: string;
+ code?: number;
+}
+
+async function main() {
+ try {
+ console.log('Testing command executor...\n');
+
+ // Test 1: Simple successful command
+ console.log('Test 1: Running "echo hello world"');
+ const result1 = await executeCommand('echo "hello world"');
+ console.log('Success:', result1, '\n');
+
+ // Test 2: Command with error
+ console.log('Test 2: Running "ls non-existent-file"');
+ try {
+ await executeCommand('ls non-existent-file');
+ } catch (error: unknown) {
+ const err = error as CommandError;
+ console.log('Error caught as expected:');
+ console.log('Message:', err.message);
+ if (err.stderr) console.log('Stderr:', err.stderr);
+ console.log('');
+ }
+
+ // Test 3: Command with timeout (using macOS compatible command)
+ console.log('Test 3: Running "ping -c 5 127.0.0.1" with 1000ms timeout');
+ try {
+ await executeCommand('ping -c 5 127.0.0.1', 1000);
+ } catch (error: unknown) {
+ const err = error as CommandError;
+ console.log('Timeout caught as expected:');
+ console.log('Message:', err.message);
+ console.log('');
+ }
+
+ // Test 4: Successful command sequence
+ console.log('Test 4: Running "date && whoami"');
+ const result4 = await executeCommand('date && whoami');
+ console.log('Success:', result4);
+
+ console.log('\nAll tests completed successfully!');
+
+ } catch (error: unknown) {
+ const err = error as CommandError;
+ console.error('\nUnexpected error in test script:');
+ console.error('Message:', err.message);
+ if (err.stderr) console.error('Stderr:', err.stderr);
+ if (err.stdout) console.error('Stdout:', err.stdout);
+ process.exit(1);
+ }
+}
+
+main();
\ No newline at end of file
diff --git a/apps/backend/scripts/setup.ts b/apps/backend/scripts/setup.ts
new file mode 100644
index 0000000..ed6b364
--- /dev/null
+++ b/apps/backend/scripts/setup.ts
@@ -0,0 +1,17 @@
+import { fileManager } from '../src/utils/fileManager';
+
+async function main() {
+ try {
+ const projectPath = await fileManager.setupProject();
+ console.log('Project created at:', projectPath);
+ console.log('\nTo clean up later, run:');
+ console.log('bun run clean');
+ console.log('or to clean a specific path:');
+ console.log('bun run clean -- /path/to/project');
+ } catch (error) {
+ console.error('Setup failed:', error instanceof Error ? error.message : error);
+ process.exit(1);
+ }
+}
+
+await main();
\ No newline at end of file
diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts
index 663963d..6ad80b8 100644
--- a/apps/backend/src/index.ts
+++ b/apps/backend/src/index.ts
@@ -1,9 +1,46 @@
import express from 'express';
+import helmet from 'helmet';
+import cors from 'cors';
import { setupProject, getSanitizedDirName, createRustProject } from './utils/fileManager';
const app = express();
+
+// Security middleware
+app.use(
+ helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ styleSrc: ["'self'", "'unsafe-inline'"],
+ scriptSrc: ["'self'"],
+ imgSrc: ["'self'", 'data:', 'https:'],
+ },
+ },
+ crossOriginEmbedderPolicy: false,
+ hsts: {
+ maxAge: 31536000,
+ includeSubDomains: true,
+ preload: true,
+ },
+ })
+);
+
+// CORS configuration
+app.use(
+ cors({
+ origin: 'http://localhost:4200',
+ credentials: true,
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+ allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
+ optionsSuccessStatus: 200,
+ })
+);
+
+// Body parsing middleware
app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+// Routes
app.get('/', (_, res) =>
res.send('Hello from Backend!' + '
' + 'The best online soroban compiler is coming...')
);
@@ -45,4 +82,24 @@ app.post('/api/test-filemanager', async (req, res) => {
}
});
-app.listen(3000, () => console.log('Server on http://localhost:3000'));
+// Error handling middleware
+app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
+ res.status(500).json({
+ error: 'Internal Server Error',
+ message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong',
+ });
+});
+
+// 404 handler
+app.use((req, res) => {
+ res.status(404).json({
+ error: 'Not Found',
+ message: `Route ${req.originalUrl} not found`,
+ });
+});
+
+// Start server
+app.listen(3000, () => {
+ console.log('Server on http://localhost:3000');
+ console.log('CORS restricted to http://localhost:4200');
+});
diff --git a/apps/backend/src/utils/commandExecutor.ts b/apps/backend/src/utils/commandExecutor.ts
index a113487..95dcdf5 100644
--- a/apps/backend/src/utils/commandExecutor.ts
+++ b/apps/backend/src/utils/commandExecutor.ts
@@ -1,164 +1,72 @@
-import { spawn, ChildProcess } from 'child_process';
+import { spawn, type SpawnOptionsWithoutStdio } from 'child_process';
-/**
- * Result of a command execution
- */
-export interface CommandResult {
- /** Exit code of the command */
- exitCode: number;
- /** Standard output */
- stdout: string;
- /** Standard error output */
- stderr: string;
- /** Whether the command was killed due to timeout */
- timedOut: boolean;
-}
+const DEFAULT_TIMEOUT = 30000;
/**
- * Options for command execution
- */
-export interface ExecuteOptions {
- /** Working directory for the command */
- cwd?: string;
- /** Environment variables */
- env?: Record;
- /** Timeout in milliseconds (default: 30000ms = 30s) */
- timeout?: number;
-}
-
-/**
- * Error thrown when a command exceeds the timeout limit
- */
-export class CommandTimeoutError extends Error {
- constructor(timeout: number) {
- super(`Command exceeded time limit of ${timeout}ms`);
- this.name = 'CommandTimeoutError';
- }
-}
-
-/**
- * Executes a shell command with timeout enforcement
- *
- * @param command - The command to execute
- * @param args - Arguments for the command
- * @param options - Execution options including timeout
- * @returns Promise that resolves to CommandResult
- * @throws CommandTimeoutError if command exceeds timeout
+ * Executes a shell command securely with a timeout
+ * @param command The command to execute
+ * @param timeout Maximum execution time in milliseconds (default: 30000)
+ * @returns Promise that resolves with the command output
+ * @throws Error with stderr content if command fails or times out
*/
export async function executeCommand(
command: string,
- args: string[] = [],
- options: ExecuteOptions = {}
-): Promise {
- const { cwd, env = process.env, timeout = 30000 } = options;
-
- return new Promise((resolve, reject) => {
- // Buffer to collect stdout and stderr
- let stdout = '';
- let stderr = '';
- let timedOut = false;
- let childProcess: ChildProcess;
+ timeout: number = DEFAULT_TIMEOUT
+): Promise {
+ // Validate inputs
+ if (typeof command !== 'string' || command.trim() === '') {
+ throw new Error('Command must be a non-empty string');
+ }
- try {
- // Spawn the child process
- childProcess = spawn(command, args, {
- cwd,
- env: { ...process.env, ...env },
- stdio: ['pipe', 'pipe', 'pipe'],
- });
+ if (typeof timeout !== 'number' || timeout <= 0) {
+ throw new Error('Timeout must be a positive number');
+ }
- // Set up timeout
- const timeoutId = setTimeout(() => {
- timedOut = true;
+ const options: SpawnOptionsWithoutStdio = {
+ shell: '/bin/bash',
+ env: { ...process.env },
+ };
- // Kill the process if it's still running
- if (childProcess && !childProcess.killed) {
- childProcess.kill('SIGTERM');
+ return new Promise((resolve, reject) => {
+ const child = spawn('bash', ['-c', command], options);
- // Force kill after 5 seconds if SIGTERM doesn't work
- setTimeout(() => {
- if (childProcess && !childProcess.killed) {
- childProcess.kill('SIGKILL');
- }
- }, 5000);
- }
+ let stdout = '';
+ let stderr = '';
+ let timeoutId: NodeJS.Timeout;
- reject(new CommandTimeoutError(timeout));
+ // Set up timeout
+ if (timeout !== Infinity) {
+ timeoutId = setTimeout(() => {
+ child.kill('SIGTERM');
+ reject(new Error(`Command timed out after ${timeout}ms`));
}, timeout);
+ }
- // Collect stdout data
- if (childProcess.stdout) {
- childProcess.stdout.on('data', (data: Buffer) => {
- stdout += data.toString();
- });
- }
-
- // Collect stderr data
- if (childProcess.stderr) {
- childProcess.stderr.on('data', (data: Buffer) => {
- stderr += data.toString();
- });
- }
-
- // Handle process completion
- childProcess.on('close', (exitCode: number | null) => {
- clearTimeout(timeoutId);
-
- // Don't resolve if we already timed out
- if (timedOut) {
- return;
- }
-
- resolve({
- exitCode: exitCode ?? -1,
- stdout: stdout.trim(),
- stderr: stderr.trim(),
- timedOut: false,
- });
- });
+ child.stdout.on('data', (data) => {
+ stdout += data.toString();
+ });
- // Handle process errors
- childProcess.on('error', (error: Error) => {
- clearTimeout(timeoutId);
+ child.stderr.on('data', (data) => {
+ stderr += data.toString();
+ });
- // Don't reject if we already timed out
- if (timedOut) {
- return;
- }
+ child.on('close', (code) => {
+ clearTimeout(timeoutId);
+ if (code === 0) {
+ resolve(stdout.trim());
+ } else {
+ const error = new Error(stderr.trim() || `Command failed with exit code ${code}`);
+ (error as any).stderr = stderr.trim();
+ (error as any).stdout = stdout.trim();
+ (error as any).code = code;
reject(error);
- });
-
- // Handle process being killed
- childProcess.on('exit', (code: number | null, signal: string | null) => {
- if (signal === 'SIGTERM' || signal === 'SIGKILL') {
- clearTimeout(timeoutId);
+ }
+ });
- // This was likely our timeout kill, but check the flag to be sure
- if (timedOut) {
- return; // The timeout handler will reject
- }
- }
- });
- } catch (error) {
- reject(error);
- }
+ child.on('error', (err) => {
+ clearTimeout(timeoutId);
+ reject(err);
+ });
});
}
-
-/**
- * Executes a command with a specific timeout and returns the result
- * This is a convenience wrapper around executeCommand
- *
- * @param command - The command to execute
- * @param args - Arguments for the command
- * @param timeoutMs - Timeout in milliseconds
- * @returns Promise that resolves to CommandResult
- */
-export async function executeCommandWithTimeout(
- command: string,
- args: string[] = [],
- timeoutMs: number = 30000
-): Promise {
- return executeCommand(command, args, { timeout: timeoutMs });
-}