A full-stack web-based file system simulator with a beautiful, interactive terminal interface. Built with TypeScript, Node.js WebSockets, and React with Next.js.
- Features
- Project Structure
- Tech Stack
- Prerequisites
- Installation
- Setup & Configuration
- Running the Project
- Available Commands
- How It Works
- Architecture
- Development
- Real-time Terminal Interface: Interactive terminal with cursor support
- Virtual File System: Create, manage, and navigate files and directories
- WebSocket Communication: Real-time bidirectional communication between frontend and backend
- Beautifully Formatted Output:
- List formatting with item counts
- Path highlighting
- Success/error indicators
- JSON and table displays
- Command History: Persistent command history across sessions
- Responsive Design: Works on desktop and tablet
- Modern UI: Dark terminal theme with color-coded responses
webshell-monorepo/
βββ packages/
β βββ backend/ # Node.js WebSocket server
β β βββ src/
β β β βββ index.ts # WebSocket server entry point
β β β βββ shell/
β β β βββ executor.ts # Command execution logic
β β β βββ parser.ts # Command parsing
β β β βββ registry.ts # Command handlers
β β β βββ state.ts # Virtual file system state
β β βββ package.json
β β βββ tsconfig.json
β β
β βββ frontend/ # Next.js React application
β β βββ src/
β β β βββ app/
β β β β βββ page.tsx
β β β β βββ layout.tsx
β β β β βββ globals.css
β β β β βββ components/
β β β β βββ terminal.tsx # Main terminal component
β β β βββ hooks/
β β β βββ webSocket.ts # WebSocket hook
β β βββ package.json
β β βββ tsconfig.json
β β βββ next.config.ts
β β
β βββ shared/ # Shared type definitions
β βββ src/
β β βββ index.ts
β β βββ types.ts # Shared TypeScript types
β βββ package.json
β βββ tsconfig.json
β
βββ package.json # Root workspace config
βββ README.md # This file
- Node.js - Runtime environment
- TypeScript - Type-safe JavaScript
- ws (WebSockets) - Real-time bidirectional communication
- npm workspaces - Monorepo management
- React 18 - UI library
- Next.js - React framework with SSR
- TypeScript - Type safety
- Tailwind CSS - Utility-first CSS framework
- TypeScript Types - Centralized type definitions for backend & frontend
Before you begin, ensure you have installed:
- Node.js v18 or higher (Download)
- npm v9 or higher (comes with Node.js)
Check your versions:
node --version
npm --versiongit clone <repository-url>
cd terminalInstall root-level dependencies and all workspace packages:
npm installThis will automatically install dependencies for:
packages/backendpackages/frontendpackages/shared
Check that all packages are installed:
npm list --depth=0The backend server runs on port 8080 by default. To change it:
- Open packages/backend/src/index.ts
- Modify the
PORTvariable:
const PORT = 8080; // Change this valueThe frontend connects to the WebSocket server at ws://localhost:8080. To change it:
- Open packages/frontend/src/app/hooks/webSocket.ts
- Modify the WebSocket URL:
const { command, isConnected, sendCommand } = useWebSocket('ws://localhost:8080');Start both frontend and backend simultaneously:
npm run devThis command:
- Starts the backend on
ws://localhost:8080 - Starts the frontend on
http://localhost:3000
npm run dev:backendThe backend will be available at ws://localhost:8080
npm run dev:frontendThe frontend will be available at http://localhost:3000 (requires backend running separately)
Build all packages:
npm run buildBuild specific packages:
npm run build:frontend
npm run build:backendThe terminal supports the following commands:
| Command | Syntax | Description |
|---|---|---|
scan |
scan |
List all files and directories in current directory |
warp |
warp <directory> |
Change to a different directory |
pwd |
pwd |
Print current working directory path |
| Command | Syntax | Description |
|---|---|---|
forge |
forge <dirname> |
Create a new directory |
murderdir |
murderdir <dirname> |
Remove a directory |
| Command | Syntax | Description |
|---|---|---|
spawn |
spawn <filename> |
Create a new file |
inject |
inject <filename> <content> |
Write content to a file |
read |
read <filename> |
Display file contents |
murder |
murder <filename> |
Delete a file |
| Command | Syntax | Description |
|---|---|---|
help |
help |
Show all available commands |
clear |
clear |
Clear terminal history |
# Navigate file system
forge myproject
warp myproject
pwd
# Create and edit files
spawn README.md
inject README.md "Hello World"
read README.md
# List contents
scan
# Go back
warp ..
# Get help
helpUser Input (Frontend)
β
Terminal Component (React)
β
WebSocket Send (Hook)
β
Backend Server (WebSocket)
β
Command Parser β Command Registry β Handler
β
Execute on Virtual FS (State)
β
Format Output (Executor)
β
WebSocket Response
β
OutputRenderer Component
β
Display to User (Pretty Formatted)
User types a command in the terminal and presses Enter.
- Input captured by hidden textarea in terminal.tsx
- Command added to history display
- Sent to backend via WebSocket
parser.ts tokenizes the command:
"spawn hello.txt" β { cmd: "spawn", args: ["hello.txt"] }
registry.ts looks up the handler function for the command and executes it with current state.
executor.ts formats the raw output into structured data:
{
format: "list", // Type of format
content: ["file1", "file2"], // Actual content
metadata: { itemCount: 2 } // Additional info
}state.ts maintains the virtual file system tree with file and directory nodes.
Backend sends structured response:
{
"type": "output",
"data": "...",
"structured": {
"format": "list",
"content": [...],
"metadata": {...}
},
"timestamp": 1234567890
}OutputRenderer component interprets format and renders beautifully:
- list β Formatted with arrows (β)
- path β Blue highlighted box
- success β Green checkmark (β)
- error β Red X (β)
- table β Bordered table
- json β Code block with syntax
All types are defined in packages/shared/src/types.ts:
// Command input from frontend
type CommandMessage = {
command: string;
timestamp: number;
};
// Structured output format
type StructuredOutput = {
format: OutputFormat; // "text" | "list" | "table" | "json" | "path" | "success" | "error"
content: string | string[] | Record<string, string>;
metadata?: {
itemCount?: number;
isEmpty?: boolean;
};
};
// Response from backend
type CommandResponse = {
type: "output" | "error" | "clear";
data?: string;
structured?: StructuredOutput;
timestamp: number;
};Files and directories are represented as nodes:
type FileNode = {
id: string; // Unique identifier
name: string; // File/directory name
type: "file" | "directory";
parent: FileNode | null; // Parent directory
children?: FileNode[]; // For directories only
content?: string; // For files only
};Backend maintains state per connection:
type ShellState = {
cwd: FileNode; // Current working directory
history: string[]; // Command history
};| Script | Purpose |
|---|---|
npm run dev |
Start both backend and frontend in dev mode |
npm run dev:backend |
Start only backend dev server |
npm run dev:frontend |
Start only frontend dev server |
npm run build |
Build all packages for production |
npm run build:backend |
Build backend only |
npm run build:frontend |
Build frontend only |
To add a new command:
- Create Handler in packages/backend/src/shell/registry.ts:
export const myCommandHandler: CommandHandler = (args, state) => {
// Command logic here
return "Output message";
};- Register Command in
commandRegistry:
export const commandRegistry: Record<string, CommandHandler> = {
mycommand: myCommandHandler,
// ... other commands
};- Add Format Logic in packages/backend/src/shell/executor.ts:
} else if (cmdName === "mycommand") {
// Format output as desired
structured = { format: "list", content: [...] };
}- Test by running and trying the command:
npm run dev
# Terminal: mycommandEnable console logs in:
- Backend: packages/backend/src/index.ts
- Frontend: packages/frontend/src/app/hooks/webSocket.ts
Problem: "WebSocket is not connected" in browser console
Solutions:
- Ensure backend is running:
npm run dev:backend - Check WebSocket URL in webSocket.ts
- Verify backend port matches (default 8080)
- Check browser console for CORS errors
Problem: "Command not found" error
Solutions:
- Check command spelling (case-sensitive)
- Verify command is registered in
commandRegistry - Check handler function exists and is exported
- Restart both frontend and backend
Problem: Error: listen EADDRINUSE
Solutions:
- Find process using port:
lsof -i :8080 - Kill process:
kill -9 <PID> - Or change PORT in index.ts
Built with β€οΈ using TypeScript, Node.js, and React