readme
This commit is contained in:
parent
c45ac9ea98
commit
7d7a545ef8
97
README.md
Normal file
97
README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# FleetCode
|
||||||
|
|
||||||
|
A desktop terminal application for running multiple CLI coding agents simultaneously, each in isolated git worktrees.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Sessions**: Run multiple coding agent sessions (Claude, Codex) in parallel
|
||||||
|
- **Git Worktree Isolation**: Each session runs in its own git worktree, keeping work isolated
|
||||||
|
- **Persistent Sessions**: Sessions persist across app restarts with automatic resumption
|
||||||
|
- **Terminal Theming**: Choose from preset themes (macOS Light/Dark, Solarized Dark, Dracula, One Dark, GitHub Dark)
|
||||||
|
- **Setup Commands**: Configure shell commands to run before the coding agent starts
|
||||||
|
- **MCP Server Management**: Add and configure Model Context Protocol (MCP) servers
|
||||||
|
- **Session Management**: Rename, close, and delete sessions with automatic worktree cleanup
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 16+
|
||||||
|
- Git
|
||||||
|
- Claude CLI (`npm install -g @anthropic-ai/claude-cli`) or Codex
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Session Creation
|
||||||
|
|
||||||
|
1. Select a project directory (must be a git repository)
|
||||||
|
2. Choose a parent branch for the worktree
|
||||||
|
3. Select your coding agent (Claude or Codex)
|
||||||
|
4. Optionally add setup commands (e.g., environment variables, source files)
|
||||||
|
5. FleetCode creates a new git worktree and spawns a terminal session
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
- **New Sessions**: Use `--session-id <uuid>` for first-time Claude sessions
|
||||||
|
- **Reopened Sessions**: Automatically resume with `--resume <uuid>`
|
||||||
|
- **Worktrees**: Each session gets its own isolated git worktree
|
||||||
|
- **Persistence**: Sessions are saved and can be reopened after closing the app
|
||||||
|
|
||||||
|
### Terminal Settings
|
||||||
|
|
||||||
|
Access settings via the gear icon (⚙️) in the sidebar:
|
||||||
|
|
||||||
|
- **Font Family**: Choose from common monospace fonts
|
||||||
|
- **Font Size**: Adjust terminal text size
|
||||||
|
- **Theme**: Select from preset color themes
|
||||||
|
- **Cursor Blink**: Toggle cursor blinking
|
||||||
|
|
||||||
|
### MCP Servers
|
||||||
|
|
||||||
|
Configure Model Context Protocol servers for enhanced agent capabilities:
|
||||||
|
|
||||||
|
- **stdio**: Direct process communication
|
||||||
|
- **SSE**: Server-sent events via HTTP
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
FleetCode/
|
||||||
|
├── main.ts # Main Electron process (PTY management, git operations)
|
||||||
|
├── renderer.ts # Renderer process (UI, terminal instances)
|
||||||
|
├── index.html # Application layout
|
||||||
|
├── styles.css # Tailwind CSS styles
|
||||||
|
└── dist/ # Compiled output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Electron**: Desktop application framework
|
||||||
|
- **xterm.js**: Terminal emulator
|
||||||
|
- **node-pty**: PTY (pseudo-terminal) spawning
|
||||||
|
- **simple-git**: Git operations (worktree management)
|
||||||
|
- **electron-store**: Persistent configuration storage
|
||||||
|
- **Tailwind CSS**: Styling
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
||||||
24
main.ts
24
main.ts
|
|
@ -7,6 +7,7 @@ import { simpleGit } from "simple-git";
|
||||||
import Store from "electron-store";
|
import Store from "electron-store";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ interface PersistedSession {
|
||||||
config: SessionConfig;
|
config: SessionConfig;
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
sessionUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow;
|
let mainWindow: BrowserWindow;
|
||||||
|
|
@ -182,6 +184,9 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
// Create git worktree
|
// Create git worktree
|
||||||
const worktreePath = await createWorktree(config.projectDir, config.parentBranch, sessionNumber);
|
const worktreePath = await createWorktree(config.projectDir, config.parentBranch, sessionNumber);
|
||||||
|
|
||||||
|
// Generate UUID for this session
|
||||||
|
const sessionUuid = uuidv4();
|
||||||
|
|
||||||
// Create persisted session metadata
|
// Create persisted session metadata
|
||||||
const persistedSession: PersistedSession = {
|
const persistedSession: PersistedSession = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
|
|
@ -190,6 +195,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
config,
|
config,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
sessionUuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to store
|
// Save to store
|
||||||
|
|
@ -235,7 +241,11 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
|
|
||||||
// Auto-run the selected coding agent
|
// Auto-run the selected coding agent
|
||||||
if (config.codingAgent === "claude") {
|
if (config.codingAgent === "claude") {
|
||||||
const claudeCmd = config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
// New session always uses --session-id
|
||||||
|
const sessionFlag = `--session-id ${sessionUuid}`;
|
||||||
|
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
|
||||||
|
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
|
||||||
|
const claudeCmd = `claude ${flags}\r`;
|
||||||
ptyProcess.write(claudeCmd);
|
ptyProcess.write(claudeCmd);
|
||||||
} else if (config.codingAgent === "codex") {
|
} else if (config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
|
|
@ -261,7 +271,11 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
|
||||||
|
|
||||||
// Auto-run the selected coding agent
|
// Auto-run the selected coding agent
|
||||||
if (config.codingAgent === "claude") {
|
if (config.codingAgent === "claude") {
|
||||||
const claudeCmd = config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
// New session always uses --session-id
|
||||||
|
const sessionFlag = `--session-id ${sessionUuid}`;
|
||||||
|
const skipPermissionsFlag = config.skipPermissions ? "--dangerously-skip-permissions" : "";
|
||||||
|
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
|
||||||
|
const claudeCmd = `claude ${flags}\r`;
|
||||||
ptyProcess.write(claudeCmd);
|
ptyProcess.write(claudeCmd);
|
||||||
} else if (config.codingAgent === "codex") {
|
} else if (config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
|
|
@ -350,7 +364,11 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.config.codingAgent === "claude") {
|
if (session.config.codingAgent === "claude") {
|
||||||
const claudeCmd = session.config.skipPermissions ? "claude --dangerously-skip-permissions\r" : "claude\r";
|
// Reopened session always uses --resume
|
||||||
|
const sessionFlag = `--resume ${session.sessionUuid}`;
|
||||||
|
const skipPermissionsFlag = session.config.skipPermissions ? "--dangerously-skip-permissions" : "";
|
||||||
|
const flags = [sessionFlag, skipPermissionsFlag].filter(f => f).join(" ");
|
||||||
|
const claudeCmd = `claude ${flags}\r`;
|
||||||
ptyProcess.write(claudeCmd);
|
ptyProcess.write(claudeCmd);
|
||||||
} else if (session.config.codingAgent === "codex") {
|
} else if (session.config.codingAgent === "codex") {
|
||||||
ptyProcess.write("codex\r");
|
ptyProcess.write("codex\r");
|
||||||
|
|
|
||||||
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -12,12 +12,14 @@
|
||||||
"electron-store": "^11.0.0",
|
"electron-store": "^11.0.0",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "^4.0.1",
|
"@electron/rebuild": "^4.0.1",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"electron": "^38.2.0",
|
"electron": "^38.2.0",
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
|
|
@ -1017,6 +1019,13 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
|
@ -5038,6 +5047,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist-node/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/rebuild": "^4.0.1",
|
"@electron/rebuild": "^4.0.1",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"electron": "^38.2.0",
|
"electron": "^38.2.0",
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
"electron-store": "^11.0.0",
|
"electron-store": "^11.0.0",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface PersistedSession {
|
||||||
config: SessionConfig;
|
config: SessionConfig;
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
sessionUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Session {
|
interface Session {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue