fleet-commander-ai 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -58
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +38 -2
- package/dist/server/config.js.map +1 -1
- package/dist/server/utils/hook-installer.d.ts.map +1 -1
- package/dist/server/utils/hook-installer.js +49 -10
- package/dist/server/utils/hook-installer.js.map +1 -1
- package/package.json +4 -1
- package/scripts/capture-screenshots.ts +478 -0
package/README.md
CHANGED
|
@@ -1,40 +1,112 @@
|
|
|
1
1
|

|
|
2
|
-
[](https://www.npmjs.com/package/fleet-commander-ai)
|
|
3
|
+
[](LICENSE)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
[](https://github.com/hubertciebiada/fleet-commander/pulls)
|
|
7
|
+
|
|
8
|
+
<div align="center">
|
|
3
9
|
|
|
4
10
|
# Fleet Commander
|
|
5
11
|
|
|
6
|
-
One-click dashboard for orchestrating multiple Claude Code agent teams across repositories
|
|
12
|
+
**One-click dashboard for orchestrating multiple Claude Code agent teams across repositories.**
|
|
13
|
+
|
|
14
|
+
*Launch, monitor, message, and shut down 15+ parallel AI agents from a single control plane.*
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- TODO: Add hero demo GIF when captured from running instance -->
|
|
21
|
+
|
|
22
|
+
> **Note:** The npm package name is `fleet-commander-ai` (the npm package name differs from the repo name). CLI binary commands remain `fleet-commander` and `fleet-commander-mcp`.
|
|
7
23
|
|
|
8
|
-
|
|
24
|
+
---
|
|
9
25
|
|
|
10
|
-
|
|
26
|
+
## Why Fleet Commander?
|
|
11
27
|
|
|
12
|
-
|
|
28
|
+
Running 15+ parallel Claude Code agents across multiple repos is chaos without a control plane. You lose track of which teams are working on what, miss CI failures, and have no way to intervene when an agent gets stuck.
|
|
13
29
|
|
|
14
|
-
|
|
30
|
+
| Capability | What it does |
|
|
31
|
+
|------------|--------------|
|
|
32
|
+
| **Launch** | One-click team launch per GitHub issue. FIFO queue with per-project concurrency limits. |
|
|
33
|
+
| **Monitor** | Real-time SSE dashboard with status, phase, PR state, CI checks, and Gantt timeline. |
|
|
34
|
+
| **Message** | Send instructions to running agents via stdin pipe. Editable PM message templates. |
|
|
35
|
+
| **Track** | GitHub polling every 30s for PRs, CI status, and merges. Auto-merge support. |
|
|
36
|
+
| **Detect** | Idle detection at 3 minutes, stuck detection at 5 minutes. Automatic status transitions. |
|
|
37
|
+
| **Scale** | Multiple projects (repos), each with independent team slots, queues, and prompt files. |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### Prerequisites
|
|
44
|
+
|
|
45
|
+
- **Node.js 20+**
|
|
46
|
+
- **GitHub CLI** -- authenticated (`gh auth login`)
|
|
15
47
|
|
|
16
|
-
|
|
48
|
+
### Option A: Install from npm (recommended)
|
|
17
49
|
|
|
18
50
|
```bash
|
|
19
|
-
npm install -g fleet-commander
|
|
51
|
+
npm install -g fleet-commander-ai
|
|
20
52
|
```
|
|
21
53
|
|
|
22
54
|
Once installed, run `fleet-commander` to start the server on port 4680 and open the dashboard.
|
|
23
55
|
|
|
24
|
-
|
|
56
|
+
Or run without installing:
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Double-click fleet-commander.bat
|
|
58
|
+
```bash
|
|
59
|
+
npx fleet-commander-ai
|
|
29
60
|
```
|
|
30
61
|
|
|
31
|
-
|
|
62
|
+
### Option B: Clone from source
|
|
63
|
+
|
|
32
64
|
```bash
|
|
65
|
+
git clone https://github.com/hubertciebiada/fleet-commander.git
|
|
66
|
+
cd fleet-commander
|
|
33
67
|
npm run launch
|
|
34
68
|
```
|
|
35
69
|
|
|
36
70
|
This installs dependencies, builds the project, starts the server on port 4680, and opens your browser.
|
|
37
71
|
|
|
72
|
+
### Windows shortcut
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Double-click fleet-commander.bat
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### First steps
|
|
79
|
+
|
|
80
|
+
1. **Add a project** -- Open the **Projects** view (`/projects`), click **Add Project**, and enter your repo path and GitHub slug (e.g., `org/repo`). Click **Install** to deploy hooks.
|
|
81
|
+
2. **Launch a team** -- Open the **Issue Tree** (`/issues`), find an issue, and click the Play button. Fleet Commander creates a worktree, spawns a Claude Code agent, and starts working.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Screenshots
|
|
86
|
+
|
|
87
|
+
<table>
|
|
88
|
+
<tr>
|
|
89
|
+
<td align="center"><strong>Fleet Grid</strong><br/><img src="docs/screenshots/fleet-grid.png" width="400" alt="Fleet Grid view showing team table with status, phase, PR, and CI columns" /></td>
|
|
90
|
+
<td align="center"><strong>Team Detail</strong><br/><img src="docs/screenshots/team-detail.png" width="400" alt="Team Detail slide-over panel with session log and event timeline" /></td>
|
|
91
|
+
</tr>
|
|
92
|
+
<tr>
|
|
93
|
+
<td align="center"><strong>Issue Tree</strong><br/><img src="docs/screenshots/issue-tree.png" width="400" alt="Issue Tree with hierarchical GitHub issues and Play button" /></td>
|
|
94
|
+
<td align="center"><strong>CommGraph</strong><br/><img src="docs/screenshots/comm-graph.png" width="400" alt="Communication graph showing agent interaction patterns" /></td>
|
|
95
|
+
</tr>
|
|
96
|
+
<tr>
|
|
97
|
+
<td align="center"><strong>Lifecycle</strong><br/><img src="docs/screenshots/lifecycle.png" width="400" alt="State machine diagram with team lifecycle transitions" /></td>
|
|
98
|
+
<td align="center"><strong>Usage</strong><br/><img src="docs/screenshots/usage.png" width="400" alt="Usage progress bars for daily, weekly, Sonnet, and extra usage" /></td>
|
|
99
|
+
</tr>
|
|
100
|
+
<tr>
|
|
101
|
+
<td align="center"><strong>Projects</strong><br/><img src="docs/screenshots/projects.png" width="400" alt="Projects page with install status, cleanup, and prompt editor" /></td>
|
|
102
|
+
<td></td>
|
|
103
|
+
</tr>
|
|
104
|
+
</table>
|
|
105
|
+
|
|
106
|
+
<p align="center"><em>Screenshots are auto-generated with <code>npm run capture-screenshots</code>. See <a href="scripts/capture-screenshots.ts">scripts/capture-screenshots.ts</a>.</em></p>
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
38
110
|
## Architecture
|
|
39
111
|
|
|
40
112
|

|
|
@@ -59,6 +131,29 @@ graph TB
|
|
|
59
131
|
|
|
60
132
|
</details>
|
|
61
133
|
|
|
134
|
+
<details>
|
|
135
|
+
<summary>How It Works</summary>
|
|
136
|
+
|
|
137
|
+
### Hook Events
|
|
138
|
+
|
|
139
|
+
Claude Code agent sessions fire lifecycle hooks (session start, tool use, notifications, errors, session end). Fleet Commander installs shell scripts into each project repo that POST these events back to the server. All hooks are fire-and-forget -- they exit 0 regardless of whether the POST succeeds, so they never block the agent.
|
|
140
|
+
|
|
141
|
+
### Stdin/Stdout Pipes
|
|
142
|
+
|
|
143
|
+
Each agent team is a `child_process.spawn()` of `claude` with `--input-format stream-json --output-format stream-json`. Fleet Commander holds the stdin pipe open to send messages (PM instructions, CI results, merge notifications) and reads stdout for real-time output streaming. The stdout stream is parsed and broadcast to the dashboard via SSE.
|
|
144
|
+
|
|
145
|
+
### SSE Push
|
|
146
|
+
|
|
147
|
+
The SSE Broker manages long-lived connections from the React dashboard. It emits 14 event types (team status changes, output chunks, PR updates, usage snapshots, heartbeats, etc.) so the UI updates in real time without polling. A 30-second heartbeat keeps connections alive through proxies.
|
|
148
|
+
|
|
149
|
+
### GitHub Polling
|
|
150
|
+
|
|
151
|
+
The GitHub Poller runs every 30 seconds and uses the `gh` CLI (not Octokit) to check for PR state changes, CI check results, and merge status. When it detects a change, it updates the database and broadcasts an SSE event. This is how the dashboard knows when CI passes or a PR is merged.
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
62
157
|
## Team Lifecycle
|
|
63
158
|
|
|
64
159
|

|
|
@@ -83,43 +178,18 @@ stateDiagram-v2
|
|
|
83
178
|
|
|
84
179
|
</details>
|
|
85
180
|
|
|
86
|
-
|
|
181
|
+
### Walkthrough
|
|
87
182
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- **GitHub polling** -- tracks PRs, CI status, and merges every 30 seconds
|
|
95
|
-
- **Stuck detection** -- flags idle teams at 3 min, stuck at 5 min
|
|
96
|
-
- **Per-project prompt files** -- configurable launch prompts with `{{ISSUE_NUMBER}}` placeholder
|
|
97
|
-
- **FIFO queue** -- max active teams per project, excess teams queue automatically
|
|
98
|
-
- **PR popover** -- CI check details and auto-merge actions inline
|
|
99
|
-
- **Log export** -- download team output as JSON or TXT
|
|
100
|
-
- **Install/uninstall scripts** -- deploy hooks, workflow template, and slash command to any repo
|
|
101
|
-
- **Windows Terminal integration** -- interactive mode via `wt.exe` for hands-on debugging
|
|
102
|
-
|
|
103
|
-
## Adding a Project
|
|
104
|
-
|
|
105
|
-
1. Open the **Projects** view (`/projects`)
|
|
106
|
-
2. Click **Add Project**
|
|
107
|
-
3. Enter the project name, local repo path, and GitHub repo slug (e.g. `org/repo`)
|
|
108
|
-
4. Set **Max Active Teams** (default: 5)
|
|
109
|
-
5. Optionally configure a custom prompt file (relative to the repo root)
|
|
110
|
-
6. Click **Save**
|
|
111
|
-
7. Click **Install** to deploy hooks and the workflow template to the repo
|
|
183
|
+
1. **Queued** -- A team is created for an issue and placed in the FIFO queue. It waits here until a slot opens (per-project concurrency limit).
|
|
184
|
+
2. **Launching** -- A slot is available. Fleet Commander creates a git worktree, spawns a Claude Code process, and waits for the first event. If no event arrives within the launch timeout (default 5 min), the team is marked failed.
|
|
185
|
+
3. **Running** -- The agent is actively working. Hook events stream in, output is captured, and the dashboard shows real-time progress.
|
|
186
|
+
4. **Idle** -- No hook events for 3 minutes. The team is flagged but continues running. Any new event transitions it back to running.
|
|
187
|
+
5. **Stuck** -- No hook events for 5 minutes (from idle). The team is flagged for PM attention. A message can be sent via stdin to nudge the agent. Any new event transitions it back to running.
|
|
188
|
+
6. **Done / Failed** -- The session ends normally (done) or the process crashes / exits non-zero (failed). The worktree and branch can be cleaned up from the Projects view.
|
|
112
189
|
|
|
113
|
-
|
|
190
|
+
**CI flow:** PR detected -> CI green/red -> message to team via stdin -> PR merged -> team marked done
|
|
114
191
|
|
|
115
|
-
|
|
116
|
-
|------|------|---------------|
|
|
117
|
-
| **Fleet Grid** | `/` | Team table with status, phase, PR, CI, duration. Toggle to Gantt timeline. |
|
|
118
|
-
| **Issue Tree** | `/issues` | GitHub issue hierarchy with search. Play button launches a team per issue. |
|
|
119
|
-
| **Usage** | `/usage` | Four progress bars: daily, weekly, Sonnet, and extra usage percentages. |
|
|
120
|
-
| **Projects** | `/projects` | CRUD for projects. Install status, cleanup, prompt editor. |
|
|
121
|
-
| **Settings** | `/settings` | Read-only viewer for current server configuration. |
|
|
122
|
-
| **Lifecycle** | `/lifecycle` | Team state machine diagram + editable PM message templates |
|
|
192
|
+
---
|
|
123
193
|
|
|
124
194
|
## Configuration
|
|
125
195
|
|
|
@@ -128,32 +198,117 @@ All settings have sensible defaults. Override via environment variables:
|
|
|
128
198
|
| Variable | Default | Description |
|
|
129
199
|
|----------|---------|-------------|
|
|
130
200
|
| `PORT` | `4680` | Server port |
|
|
201
|
+
| `FLEET_HOST` | `0.0.0.0` | Network interface to bind to |
|
|
131
202
|
| `FLEET_IDLE_THRESHOLD_MIN` | `3` | Minutes before a team is marked idle |
|
|
132
203
|
| `FLEET_STUCK_THRESHOLD_MIN` | `5` | Minutes before a team is marked stuck |
|
|
204
|
+
| `FLEET_LAUNCH_TIMEOUT_MIN` | `5` | Minutes before a launching team is marked failed |
|
|
133
205
|
| `FLEET_MAX_CI_FAILURES` | `3` | Unique CI failure types before blocking |
|
|
206
|
+
| `FLEET_EARLY_CRASH_THRESHOLD_SEC` | `120` | Seconds before a SubagentStop is considered an early crash |
|
|
207
|
+
| `FLEET_EARLY_CRASH_MIN_TOOLS` | `5` | Minimum tool-use events for a subagent to be considered healthy |
|
|
134
208
|
| `FLEET_GITHUB_POLL_MS` | `30000` | GitHub polling interval (ms) |
|
|
135
209
|
| `FLEET_DB_PATH` | `./fleet.db` | SQLite database file path |
|
|
136
|
-
| `FLEET_TERMINAL` | `auto` | Windows terminal: `auto`, `wt`, or `cmd` |
|
|
137
|
-
| `
|
|
210
|
+
| `FLEET_TERMINAL` | `auto` | Windows terminal preference: `auto`, `wt`, or `cmd` |
|
|
211
|
+
| `FLEET_CLAUDE_CMD` | `claude` | Claude Code CLI command |
|
|
212
|
+
| `FLEET_SKIP_PERMISSIONS` | `true` | Skip CC permission prompts (`--dangerously-skip-permissions`) |
|
|
213
|
+
| `LOG_LEVEL` | `info` | Server log level |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Built With
|
|
218
|
+
|
|
219
|
+
| Layer | Technology |
|
|
220
|
+
|-------|------------|
|
|
221
|
+
| Server | [Fastify 5](https://fastify.dev/) on Node.js 20+ |
|
|
222
|
+
| Client | [React 19](https://react.dev/) with [Vite 6](https://vite.dev/) |
|
|
223
|
+
| Language | [TypeScript 5.7](https://www.typescriptlang.org/) |
|
|
224
|
+
| Database | [SQLite](https://sqlite.org/) via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), WAL mode |
|
|
225
|
+
| Styling | [Tailwind CSS](https://tailwindcss.com/) (GitHub-dark theme) |
|
|
226
|
+
| Real-time | Server-Sent Events (SSE) |
|
|
227
|
+
| Agent interface | Claude Code CLI (`--input-format stream-json`, `--output-format stream-json`) |
|
|
228
|
+
| GitHub | [`gh` CLI](https://cli.github.com/) for all GitHub operations |
|
|
229
|
+
| Testing | [Vitest](https://vitest.dev/), [Testing Library](https://testing-library.com/) |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## MCP Integration
|
|
234
|
+
|
|
235
|
+
Fleet Commander exposes tools via the [Model Context Protocol](https://modelcontextprotocol.io/) over stdio transport. This allows Claude Code (or any MCP-compatible client) to query fleet state, launch teams, send messages, and inspect timelines programmatically.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Add as an MCP server in Claude Code
|
|
239
|
+
claude mcp add fleet-commander -- node bin/fleet-commander-mcp.js
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Or add a `.mcp.json` to your project root:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"mcpServers": {
|
|
247
|
+
"fleet-commander": {
|
|
248
|
+
"command": "node",
|
|
249
|
+
"args": ["bin/fleet-commander-mcp.js"],
|
|
250
|
+
"cwd": "/path/to/fleet-commander"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
All MCP tools use the `fleet_` prefix (e.g., `fleet_system_health`, `fleet_list_teams`, `fleet_send_message`). See [docs/mcp.md](docs/mcp.md) for the full tool reference and developer guide.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Views
|
|
261
|
+
|
|
262
|
+
| View | Path | What it shows |
|
|
263
|
+
|------|------|---------------|
|
|
264
|
+
| **Fleet Grid** | `/` | Team table with status, phase, PR, CI, duration. Toggle to Gantt timeline. |
|
|
265
|
+
| **Issue Tree** | `/issues` | GitHub issue hierarchy with search. Play button launches a team per issue. |
|
|
266
|
+
| **Usage** | `/usage` | Four progress bars: daily, weekly, Sonnet, and extra usage percentages. |
|
|
267
|
+
| **Projects** | `/projects` | CRUD for projects. Install status, cleanup, prompt editor. |
|
|
268
|
+
| **Lifecycle** | `/lifecycle` | Team state machine diagram + editable PM message templates. |
|
|
269
|
+
| **Settings** | `/settings` | Read-only viewer for current server configuration. |
|
|
270
|
+
|
|
271
|
+
---
|
|
138
272
|
|
|
139
273
|
## Development
|
|
140
274
|
|
|
141
275
|
| Command | Description |
|
|
142
276
|
|---------|-------------|
|
|
143
277
|
| `npm run dev` | Start dev server + Vite HMR |
|
|
144
|
-
| `npm run build` | Production build |
|
|
145
|
-
| `npm start` | Production server |
|
|
146
|
-
| `npm test` | Run
|
|
278
|
+
| `npm run build` | Production build (tsc + vite) |
|
|
279
|
+
| `npm start` | Production server (`node dist/server/index.js`) |
|
|
280
|
+
| `npm test` | Run all tests (vitest) |
|
|
147
281
|
| `npm run test:client` | Client-only tests |
|
|
148
282
|
| `npm run test:e2e` | End-to-end smoke test |
|
|
149
283
|
| `npm run launch` | Auto-install, build, open browser |
|
|
284
|
+
| `npm run capture-screenshots` | Generate screenshots with Playwright |
|
|
150
285
|
|
|
151
|
-
|
|
286
|
+
---
|
|
152
287
|
|
|
153
|
-
|
|
154
|
-
|
|
288
|
+
## Contributing
|
|
289
|
+
|
|
290
|
+
Contributions are welcome. To get started:
|
|
291
|
+
|
|
292
|
+
1. **Fork** the repository
|
|
293
|
+
2. **Create a branch** from `main` (`git checkout -b feat/your-feature`)
|
|
294
|
+
3. **Make your changes** and add tests
|
|
295
|
+
4. **Run the test suite** (`npm test`)
|
|
296
|
+
5. **Commit** with a clear message (`git commit -m "Add your feature"`)
|
|
297
|
+
6. **Push** to your fork (`git push origin feat/your-feature`)
|
|
298
|
+
7. **Open a Pull Request** against `main`
|
|
299
|
+
|
|
300
|
+
Please follow the existing code style and conventions. See [CLAUDE.md](CLAUDE.md) for project-specific guidelines.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
[Apache-2.0](LICENSE)
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
<div align="center">
|
|
155
311
|
|
|
156
|
-
|
|
312
|
+
[](https://star-history.com/#hubertciebiada/fleet-commander&Date)
|
|
157
313
|
|
|
158
|
-
|
|
159
|
-
- Interactive terminal mode (Windows Terminal) is Windows-only
|
|
314
|
+
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"AAiDA,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMhE;AAID,QAAA,MAAM,MAAM;;;IAIV,+DAA+D;;;;;;;;;;IAa/D,qFAAqF;;IAGrF,8EAA8E;;;;;;;;;;;;;;;;;;;;;;IAiC9E;;;;;OAKG;iBACuD,MAAM,GAAG,IAAI,GAAG,KAAK;EAC/E,CAAC;AAGH,wBAAgB,cAAc,IAAI,IAAI,CAuCrC;AAID,eAAe,MAAM,CAAC;AACtB,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC"}
|
package/dist/server/config.js
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
/**
|
|
6
|
+
* Determine the Fleet Commander package root directory.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. FLEET_COMMANDER_ROOT env var (set by bin/fleet-commander.js for npm global installs)
|
|
10
|
+
* 2. Walk up from this file's location to find package.json (works for both
|
|
11
|
+
* development `src/server/config.ts` and compiled `dist/server/config.js`)
|
|
12
|
+
* 3. `git rev-parse --show-toplevel` (for development from a git clone)
|
|
13
|
+
* 4. process.cwd() as last resort
|
|
14
|
+
*/
|
|
3
15
|
function findFleetCommanderRoot() {
|
|
16
|
+
// Strategy 1: Walk up from __dirname to find the package root (has package.json + hooks/)
|
|
17
|
+
// This works whether running from src/ (dev) or dist/ (compiled), and critically,
|
|
18
|
+
// from an npm global install where there is no git repo.
|
|
4
19
|
try {
|
|
5
|
-
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
let dir = path.dirname(__filename);
|
|
22
|
+
for (let i = 0; i < 5; i++) {
|
|
23
|
+
if (fs.existsSync(path.join(dir, 'package.json')) &&
|
|
24
|
+
fs.existsSync(path.join(dir, 'hooks'))) {
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir);
|
|
28
|
+
if (parent === dir)
|
|
29
|
+
break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// import.meta.url not available or other error
|
|
35
|
+
}
|
|
36
|
+
// Strategy 2: git rev-parse (normalize the path for Windows compatibility —
|
|
37
|
+
// git returns POSIX paths like /c/Users/... which path.join can't handle)
|
|
38
|
+
try {
|
|
39
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
40
|
+
return path.resolve(gitRoot);
|
|
6
41
|
}
|
|
7
42
|
catch {
|
|
8
|
-
|
|
43
|
+
// Not in a git repo
|
|
9
44
|
}
|
|
45
|
+
return process.cwd();
|
|
10
46
|
}
|
|
11
47
|
/** Parse an integer from a string, throwing if the result is NaN. */
|
|
12
48
|
export function safeParseInt(value, name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/server/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-installer.d.ts","sourceRoot":"","sources":["../../../src/server/utils/hook-installer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"hook-installer.d.ts","sourceRoot":"","sources":["../../../src/server/utils/hook-installer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAmBjD,wBAAgB,UAAU,IAAI,MAAM,CAgDnC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA+BzH;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAkBhF"}
|
|
@@ -11,26 +11,63 @@ import config from '../config.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Find Git Bash executable on Windows.
|
|
13
13
|
* Avoids WSL's bash which can't handle Windows paths.
|
|
14
|
+
*
|
|
15
|
+
* `git --exec-path` returns something like:
|
|
16
|
+
* - "C:/Program Files/Git/mingw64/libexec/git-core" (standard install, 3 levels deep)
|
|
17
|
+
* - "C:/Git/scm/mingw64/libexec/git-core" (custom install, 3 levels deep)
|
|
18
|
+
* - "C:/Git/scm/libexec/git-core" (portable, 2 levels deep)
|
|
19
|
+
*
|
|
20
|
+
* We walk up the tree until we find usr/bin/bash.exe rather than assuming
|
|
21
|
+
* a fixed depth, since the layout varies across Git for Windows installs.
|
|
14
22
|
*/
|
|
15
23
|
let _gitBash = null;
|
|
16
24
|
export function getGitBash() {
|
|
17
25
|
if (_gitBash)
|
|
18
26
|
return _gitBash;
|
|
27
|
+
// Strategy 1: Walk up from git --exec-path to find usr/bin/bash.exe
|
|
19
28
|
try {
|
|
20
|
-
// git --exec-path returns e.g. "C:/Git/scm/libexec/git-core"
|
|
21
|
-
// Git bash is at {git_root}/usr/bin/bash.exe
|
|
22
29
|
const execPath = execSync('git --exec-path', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
let dir = path.resolve(execPath);
|
|
31
|
+
// Walk up at most 5 levels looking for usr/bin/bash.exe
|
|
32
|
+
for (let i = 0; i < 5; i++) {
|
|
33
|
+
const candidate = path.join(dir, 'usr', 'bin', 'bash.exe');
|
|
34
|
+
if (fs.existsSync(candidate)) {
|
|
35
|
+
_gitBash = candidate;
|
|
36
|
+
return _gitBash;
|
|
37
|
+
}
|
|
38
|
+
const parent = path.dirname(dir);
|
|
39
|
+
if (parent === dir)
|
|
40
|
+
break; // reached filesystem root
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// git not in PATH or other error — try fallback locations
|
|
46
|
+
}
|
|
47
|
+
// Strategy 2: Check common Git for Windows install locations
|
|
48
|
+
const commonLocations = [
|
|
49
|
+
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
50
|
+
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
|
51
|
+
];
|
|
52
|
+
for (const loc of commonLocations) {
|
|
53
|
+
if (fs.existsSync(loc)) {
|
|
54
|
+
_gitBash = loc;
|
|
55
|
+
return _gitBash;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Strategy 3: Check if 'bash' in PATH is Git Bash (not WSL)
|
|
59
|
+
// by looking for the --version containing "pc-msys" or "pc-linux"
|
|
60
|
+
try {
|
|
61
|
+
const ver = execSync('bash --version', { encoding: 'utf-8', stdio: 'pipe', timeout: 5000 });
|
|
62
|
+
if (ver.includes('pc-msys') || ver.includes('Msys') || ver.includes('mintty')) {
|
|
63
|
+
_gitBash = 'bash';
|
|
27
64
|
return _gitBash;
|
|
28
65
|
}
|
|
29
66
|
}
|
|
30
67
|
catch {
|
|
31
|
-
//
|
|
68
|
+
// bash not found at all
|
|
32
69
|
}
|
|
33
|
-
_gitBash = 'bash'; //
|
|
70
|
+
_gitBash = 'bash'; // last resort — may be WSL, but nothing else to try
|
|
34
71
|
return _gitBash;
|
|
35
72
|
}
|
|
36
73
|
/**
|
|
@@ -49,10 +86,12 @@ export function installHooks(repoPath, logger) {
|
|
|
49
86
|
if (!fs.existsSync(scriptPath)) {
|
|
50
87
|
return fail(`install.sh not found at ${scriptPath}`);
|
|
51
88
|
}
|
|
52
|
-
// On Windows,
|
|
89
|
+
// On Windows, use Git Bash with forward-slash paths (Git Bash handles C:/ natively)
|
|
90
|
+
const bash = getGitBash();
|
|
53
91
|
const cmd = process.platform === 'win32'
|
|
54
|
-
? `"${
|
|
92
|
+
? `"${bash}" "${toBashPath(scriptPath)}" "${toBashPath(repoPath)}"`
|
|
55
93
|
: `"${scriptPath}" "${repoPath}"`;
|
|
94
|
+
logger.info(`[installHooks] bash=${bash}, cmd=${cmd}`);
|
|
56
95
|
try {
|
|
57
96
|
const stdout = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe', timeout: 30000 });
|
|
58
97
|
return { ok: true, stdout, stderr: '' };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-installer.js","sourceRoot":"","sources":["../../../src/server/utils/hook-installer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qDAAqD;AACrD,gFAAgF;AAChF,yEAAyE;AACzE,4EAA4E;AAC5E,gFAAgF;AAGhF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,cAAc,CAAC;AAElC
|
|
1
|
+
{"version":3,"file":"hook-installer.js","sourceRoot":"","sources":["../../../src/server/utils/hook-installer.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qDAAqD;AACrD,gFAAgF;AAChF,yEAAyE;AACzE,4EAA4E;AAC5E,gFAAgF;AAGhF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,cAAc,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,IAAI,QAAQ,GAAkB,IAAI,CAAC;AACnC,MAAM,UAAU,UAAU;IACxB,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,oEAAoE;IACpE,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,QAAQ,CAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1F,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,wDAAwD;QACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;YAC3D,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC7B,QAAQ,GAAG,SAAS,CAAC;gBACrB,OAAO,QAAQ,CAAC;YAClB,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,MAAM,KAAK,GAAG;gBAAE,MAAM,CAAC,0BAA0B;YACrD,GAAG,GAAG,MAAM,CAAC;QACf,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;IAED,6DAA6D;IAC7D,MAAM,eAAe,GAAG;QACtB,4CAA4C;QAC5C,kDAAkD;KACnD,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,QAAQ,GAAG,GAAG,CAAC;YACf,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,kEAAkE;IAClE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,QAAQ,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5F,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9E,QAAQ,GAAG,MAAM,CAAC;YAClB,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,wBAAwB;IAC1B,CAAC;IAED,QAAQ,GAAG,MAAM,CAAC,CAAC,oDAAoD;IACvE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,MAAyB;IACtE,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IACjF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,2BAA2B,UAAU,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,oFAAoF;IACpF,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO;QACtC,CAAC,CAAC,IAAI,IAAI,MAAM,UAAU,CAAC,UAAU,CAAC,MAAM,UAAU,CAAC,QAAQ,CAAC,GAAG;QACnE,CAAC,CAAC,IAAI,UAAU,MAAM,QAAQ,GAAG,CAAC;IAEpC,MAAM,CAAC,IAAI,CAAC,uBAAuB,IAAI,SAAS,GAAG,EAAE,CAAC,CAAC;IAEvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,GAA8E,CAAC;QACzF,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;QAC9B,MAAM,CAAC,KAAK,CACV,6BAA6B,QAAQ,UAAU,CAAC,CAAC,MAAM,IAAI,GAAG,MAAM;YACpE,UAAU,GAAG,IAAI;YACjB,aAAa,MAAM,CAAC,IAAI,EAAE,IAAI;YAC9B,aAAa,MAAM,CAAC,IAAI,EAAE,EAAE,CAC7B,CAAC;QACF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,CAAC,OAAO,IAAI,eAAe,EAAE,CAAC;IAC/E,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAyB;IACxE,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACnF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO;YACtC,CAAC,CAAC,IAAI,UAAU,EAAE,MAAM,UAAU,CAAC,UAAU,CAAC,MAAM,UAAU,CAAC,QAAQ,CAAC,GAAG;YAC3E,CAAC,CAAC,IAAI,UAAU,MAAM,QAAQ,GAAG,CAAC;QAEpC,QAAQ,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mDAAmD;QACnD,MAAM,CAAC,KAAK,CACV,6CAA6C,QAAQ,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC7G,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fleet-commander-ai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Web dashboard for orchestrating Claude Code agent teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
+
"fleet-commander-ai": "./bin/fleet-commander.js",
|
|
8
9
|
"fleet-commander": "./bin/fleet-commander.js",
|
|
9
10
|
"fleet-commander-mcp": "./bin/fleet-commander-mcp.js"
|
|
10
11
|
},
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"test:client": "vitest run --config vitest.client.config.ts",
|
|
54
55
|
"test:watch": "vitest",
|
|
55
56
|
"test:e2e": "bash tests/e2e/smoke-test.sh",
|
|
57
|
+
"capture-screenshots": "npx tsx scripts/capture-screenshots.ts",
|
|
56
58
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
|
|
57
59
|
"prepack": "npm run build",
|
|
58
60
|
"prepublishOnly": "echo 'Tests already run in CI — skipping'"
|
|
@@ -80,6 +82,7 @@
|
|
|
80
82
|
"autoprefixer": "^10.4.21",
|
|
81
83
|
"concurrently": "^9.1.2",
|
|
82
84
|
"jsdom": "^29.0.0",
|
|
85
|
+
"playwright": "^1.52.0",
|
|
83
86
|
"postcss": "^8.5.3",
|
|
84
87
|
"react": "^19.0.0",
|
|
85
88
|
"react-dom": "^19.0.0",
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright screenshot capture script for Fleet Commander README.
|
|
3
|
+
*
|
|
4
|
+
* Seeds a temporary SQLite database with representative data (teams in various
|
|
5
|
+
* states, PRs, events), starts the Fastify server, navigates to each view,
|
|
6
|
+
* and captures screenshots at 1280x800. Screenshots are saved to
|
|
7
|
+
* docs/screenshots/ for use in README.md.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx scripts/capture-screenshots.ts
|
|
11
|
+
*
|
|
12
|
+
* Prerequisites:
|
|
13
|
+
* npm install --save-dev playwright
|
|
14
|
+
* npx playwright install chromium
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { chromium, type Browser, type Page } from 'playwright';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
|
|
26
|
+
const SCREENSHOT_DIR = path.join(ROOT, 'docs', 'screenshots');
|
|
27
|
+
const TEMP_DB = path.join(ROOT, 'fleet-screenshots-temp.db');
|
|
28
|
+
const PORT = 14680; // Use a non-standard port to avoid conflicts
|
|
29
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
30
|
+
|
|
31
|
+
const VIEWPORT = { width: 1280, height: 800 };
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Seed data
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
interface SeedProject {
|
|
38
|
+
name: string;
|
|
39
|
+
repoPath: string;
|
|
40
|
+
githubRepo: string;
|
|
41
|
+
maxActiveTeams: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SeedTeam {
|
|
45
|
+
issueNumber: number;
|
|
46
|
+
issueTitle: string;
|
|
47
|
+
projectId: number;
|
|
48
|
+
worktreeName: string;
|
|
49
|
+
branchName: string;
|
|
50
|
+
status: string;
|
|
51
|
+
phase: string;
|
|
52
|
+
prNumber: number | null;
|
|
53
|
+
launchedAt: string;
|
|
54
|
+
lastEventAt: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface SeedPR {
|
|
58
|
+
prNumber: number;
|
|
59
|
+
teamId: number;
|
|
60
|
+
title: string;
|
|
61
|
+
state: string;
|
|
62
|
+
ciStatus: string;
|
|
63
|
+
mergeStatus: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SeedEvent {
|
|
67
|
+
teamId: number;
|
|
68
|
+
eventType: string;
|
|
69
|
+
toolName: string | null;
|
|
70
|
+
payload: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SEED_PROJECTS: SeedProject[] = [
|
|
74
|
+
{
|
|
75
|
+
name: 'fleet-commander',
|
|
76
|
+
repoPath: '/repos/fleet-commander',
|
|
77
|
+
githubRepo: 'hubertciebiada/fleet-commander',
|
|
78
|
+
maxActiveTeams: 5,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'my-web-app',
|
|
82
|
+
repoPath: '/repos/my-web-app',
|
|
83
|
+
githubRepo: 'org/my-web-app',
|
|
84
|
+
maxActiveTeams: 3,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
function minutesAgo(min: number): string {
|
|
89
|
+
return new Date(Date.now() - min * 60_000).toISOString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SEED_TEAMS: SeedTeam[] = [
|
|
93
|
+
{
|
|
94
|
+
issueNumber: 274,
|
|
95
|
+
issueTitle: 'README overhaul with screenshots',
|
|
96
|
+
projectId: 1,
|
|
97
|
+
worktreeName: 'fleet-commander-274',
|
|
98
|
+
branchName: 'feat/274-readme-overhaul',
|
|
99
|
+
status: 'running',
|
|
100
|
+
phase: 'implementing',
|
|
101
|
+
prNumber: 301,
|
|
102
|
+
launchedAt: minutesAgo(45),
|
|
103
|
+
lastEventAt: minutesAgo(1),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
issueNumber: 330,
|
|
107
|
+
issueTitle: 'MCP tool reference documentation',
|
|
108
|
+
projectId: 1,
|
|
109
|
+
worktreeName: 'fleet-commander-330',
|
|
110
|
+
branchName: 'feat/330-mcp-docs',
|
|
111
|
+
status: 'running',
|
|
112
|
+
phase: 'reviewing',
|
|
113
|
+
prNumber: 302,
|
|
114
|
+
launchedAt: minutesAgo(30),
|
|
115
|
+
lastEventAt: minutesAgo(2),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
issueNumber: 105,
|
|
119
|
+
issueTitle: 'Fix CI pipeline for Windows',
|
|
120
|
+
projectId: 1,
|
|
121
|
+
worktreeName: 'fleet-commander-105',
|
|
122
|
+
branchName: 'fix/105-ci-windows',
|
|
123
|
+
status: 'idle',
|
|
124
|
+
phase: 'implementing',
|
|
125
|
+
prNumber: 303,
|
|
126
|
+
launchedAt: minutesAgo(60),
|
|
127
|
+
lastEventAt: minutesAgo(4),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
issueNumber: 42,
|
|
131
|
+
issueTitle: 'Add search feature to dashboard',
|
|
132
|
+
projectId: 2,
|
|
133
|
+
worktreeName: 'my-web-app-42',
|
|
134
|
+
branchName: 'feat/42-add-search',
|
|
135
|
+
status: 'done',
|
|
136
|
+
phase: 'done',
|
|
137
|
+
prNumber: 201,
|
|
138
|
+
launchedAt: minutesAgo(120),
|
|
139
|
+
lastEventAt: minutesAgo(90),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
issueNumber: 55,
|
|
143
|
+
issueTitle: 'Refactor authentication module',
|
|
144
|
+
projectId: 2,
|
|
145
|
+
worktreeName: 'my-web-app-55',
|
|
146
|
+
branchName: 'feat/55-refactor-auth',
|
|
147
|
+
status: 'stuck',
|
|
148
|
+
phase: 'implementing',
|
|
149
|
+
prNumber: null,
|
|
150
|
+
launchedAt: minutesAgo(90),
|
|
151
|
+
lastEventAt: minutesAgo(8),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
issueNumber: 200,
|
|
155
|
+
issueTitle: 'Upgrade dependencies to latest',
|
|
156
|
+
projectId: 1,
|
|
157
|
+
worktreeName: 'fleet-commander-200',
|
|
158
|
+
branchName: 'chore/200-upgrade-deps',
|
|
159
|
+
status: 'queued',
|
|
160
|
+
phase: 'init',
|
|
161
|
+
prNumber: null,
|
|
162
|
+
launchedAt: minutesAgo(5),
|
|
163
|
+
lastEventAt: minutesAgo(5),
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const SEED_PRS: SeedPR[] = [
|
|
168
|
+
{ prNumber: 301, teamId: 1, title: 'feat: README overhaul with screenshots', state: 'OPEN', ciStatus: 'passing', mergeStatus: 'clean' },
|
|
169
|
+
{ prNumber: 302, teamId: 2, title: 'feat: MCP tool reference documentation', state: 'OPEN', ciStatus: 'pending', mergeStatus: 'clean' },
|
|
170
|
+
{ prNumber: 303, teamId: 3, title: 'fix: CI pipeline for Windows', state: 'OPEN', ciStatus: 'failing', mergeStatus: 'dirty' },
|
|
171
|
+
{ prNumber: 201, teamId: 4, title: 'feat: Add search feature', state: 'MERGED', ciStatus: 'passing', mergeStatus: 'clean' },
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const SEED_EVENTS: SeedEvent[] = [
|
|
175
|
+
{ teamId: 1, eventType: 'session_start', toolName: null, payload: '{}' },
|
|
176
|
+
{ teamId: 1, eventType: 'tool_use', toolName: 'Read', payload: '{"file":"README.md"}' },
|
|
177
|
+
{ teamId: 1, eventType: 'tool_use', toolName: 'Edit', payload: '{"file":"README.md"}' },
|
|
178
|
+
{ teamId: 1, eventType: 'tool_use', toolName: 'Write', payload: '{"file":"scripts/capture-screenshots.ts"}' },
|
|
179
|
+
{ teamId: 2, eventType: 'session_start', toolName: null, payload: '{}' },
|
|
180
|
+
{ teamId: 2, eventType: 'tool_use', toolName: 'Read', payload: '{"file":"docs/mcp.md"}' },
|
|
181
|
+
{ teamId: 3, eventType: 'session_start', toolName: null, payload: '{}' },
|
|
182
|
+
{ teamId: 3, eventType: 'tool_use', toolName: 'Bash', payload: '{"command":"npm test"}' },
|
|
183
|
+
{ teamId: 4, eventType: 'session_start', toolName: null, payload: '{}' },
|
|
184
|
+
{ teamId: 4, eventType: 'session_end', toolName: null, payload: '{}' },
|
|
185
|
+
{ teamId: 5, eventType: 'session_start', toolName: null, payload: '{}' },
|
|
186
|
+
{ teamId: 5, eventType: 'tool_use', toolName: 'Grep', payload: '{"pattern":"auth"}' },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Database seeding via better-sqlite3
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
async function seedDatabase(): Promise<void> {
|
|
194
|
+
// Clean up any previous temp DB
|
|
195
|
+
if (fs.existsSync(TEMP_DB)) {
|
|
196
|
+
fs.unlinkSync(TEMP_DB);
|
|
197
|
+
}
|
|
198
|
+
if (fs.existsSync(`${TEMP_DB}-wal`)) {
|
|
199
|
+
fs.unlinkSync(`${TEMP_DB}-wal`);
|
|
200
|
+
}
|
|
201
|
+
if (fs.existsSync(`${TEMP_DB}-shm`)) {
|
|
202
|
+
fs.unlinkSync(`${TEMP_DB}-shm`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Use better-sqlite3 dynamically — the same driver the app uses
|
|
206
|
+
const Database = (await import('better-sqlite3')).default;
|
|
207
|
+
const db = new Database(TEMP_DB);
|
|
208
|
+
db.pragma('journal_mode = WAL');
|
|
209
|
+
|
|
210
|
+
// Apply schema
|
|
211
|
+
const schemaPath = path.join(ROOT, 'src', 'server', 'schema.sql');
|
|
212
|
+
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
|
213
|
+
db.exec(schema);
|
|
214
|
+
|
|
215
|
+
// Insert projects
|
|
216
|
+
const insertProject = db.prepare(
|
|
217
|
+
`INSERT INTO projects (name, repo_path, github_repo, max_active_teams, hooks_installed)
|
|
218
|
+
VALUES (?, ?, ?, ?, 1)`
|
|
219
|
+
);
|
|
220
|
+
for (const p of SEED_PROJECTS) {
|
|
221
|
+
insertProject.run(p.name, p.repoPath, p.githubRepo, p.maxActiveTeams);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Insert teams
|
|
225
|
+
const insertTeam = db.prepare(
|
|
226
|
+
`INSERT INTO teams (issue_number, issue_title, project_id, worktree_name, branch_name,
|
|
227
|
+
status, phase, pr_number, launched_at, last_event_at)
|
|
228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
229
|
+
);
|
|
230
|
+
for (const t of SEED_TEAMS) {
|
|
231
|
+
insertTeam.run(
|
|
232
|
+
t.issueNumber, t.issueTitle, t.projectId, t.worktreeName, t.branchName,
|
|
233
|
+
t.status, t.phase, t.prNumber, t.launchedAt, t.lastEventAt
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Insert PRs
|
|
238
|
+
const insertPR = db.prepare(
|
|
239
|
+
`INSERT INTO pull_requests (pr_number, team_id, title, state, ci_status, merge_status)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
241
|
+
);
|
|
242
|
+
for (const pr of SEED_PRS) {
|
|
243
|
+
insertPR.run(pr.prNumber, pr.teamId, pr.title, pr.state, pr.ciStatus, pr.mergeStatus);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Insert events
|
|
247
|
+
const insertEvent = db.prepare(
|
|
248
|
+
`INSERT INTO events (team_id, event_type, tool_name, payload) VALUES (?, ?, ?, ?)`
|
|
249
|
+
);
|
|
250
|
+
for (const e of SEED_EVENTS) {
|
|
251
|
+
insertEvent.run(e.teamId, e.eventType, e.toolName, e.payload);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Insert usage snapshots
|
|
255
|
+
db.prepare(
|
|
256
|
+
`INSERT INTO usage_snapshots (daily_percent, weekly_percent, sonnet_percent, extra_percent)
|
|
257
|
+
VALUES (?, ?, ?, ?)`
|
|
258
|
+
).run(45, 30, 60, 5);
|
|
259
|
+
|
|
260
|
+
db.close();
|
|
261
|
+
console.log(`Seeded temporary database: ${TEMP_DB}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Server management
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
let serverProcess: ReturnType<typeof import('child_process').spawn> | null = null;
|
|
269
|
+
|
|
270
|
+
async function startServer(): Promise<void> {
|
|
271
|
+
const { spawn } = await import('child_process');
|
|
272
|
+
|
|
273
|
+
const serverEntry = path.join(ROOT, 'dist', 'server', 'index.js');
|
|
274
|
+
if (!fs.existsSync(serverEntry)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Server entry not found at ${serverEntry}. Run "npm run build" first.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
serverProcess = spawn('node', [serverEntry], {
|
|
281
|
+
cwd: ROOT,
|
|
282
|
+
env: {
|
|
283
|
+
...process.env,
|
|
284
|
+
PORT: String(PORT),
|
|
285
|
+
FLEET_DB_PATH: TEMP_DB,
|
|
286
|
+
LOG_LEVEL: 'warn',
|
|
287
|
+
FLEET_GITHUB_POLL_MS: '999999999', // Disable polling
|
|
288
|
+
FLEET_STUCK_CHECK_MS: '999999999', // Disable stuck detection
|
|
289
|
+
FLEET_USAGE_POLL_MS: '999999999', // Disable usage polling
|
|
290
|
+
FLEET_ISSUE_POLL_MS: '999999999', // Disable issue polling
|
|
291
|
+
},
|
|
292
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
serverProcess.stderr?.on('data', (data: Buffer) => {
|
|
296
|
+
const msg = data.toString();
|
|
297
|
+
if (msg.includes('Error') || msg.includes('error')) {
|
|
298
|
+
console.error('[server stderr]', msg.trim());
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Wait for the server to be ready
|
|
303
|
+
const maxWait = 30_000;
|
|
304
|
+
const start = Date.now();
|
|
305
|
+
while (Date.now() - start < maxWait) {
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(`${BASE_URL}/api/health`);
|
|
308
|
+
if (response.ok) {
|
|
309
|
+
console.log(`Server ready on port ${PORT}`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Server not ready yet
|
|
314
|
+
}
|
|
315
|
+
await sleep(500);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
throw new Error(`Server failed to start within ${maxWait / 1000}s`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function stopServer(): void {
|
|
322
|
+
if (serverProcess) {
|
|
323
|
+
serverProcess.kill('SIGTERM');
|
|
324
|
+
serverProcess = null;
|
|
325
|
+
console.log('Server stopped');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function sleep(ms: number): Promise<void> {
|
|
330
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Screenshot capture
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
interface ScreenshotSpec {
|
|
338
|
+
name: string;
|
|
339
|
+
path: string;
|
|
340
|
+
beforeCapture?: (page: Page) => Promise<void>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const SCREENSHOTS: ScreenshotSpec[] = [
|
|
344
|
+
{
|
|
345
|
+
name: 'fleet-grid',
|
|
346
|
+
path: '/',
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: 'team-detail',
|
|
350
|
+
path: '/',
|
|
351
|
+
beforeCapture: async (page: Page) => {
|
|
352
|
+
// Click on the first running team row to open the detail panel
|
|
353
|
+
const teamRow = page.locator('tr').filter({ hasText: 'README overhaul' }).first();
|
|
354
|
+
if (await teamRow.isVisible({ timeout: 5000 })) {
|
|
355
|
+
await teamRow.click();
|
|
356
|
+
await sleep(1000); // Wait for slide-over animation
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'issue-tree',
|
|
362
|
+
path: '/issues',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'comm-graph',
|
|
366
|
+
path: '/',
|
|
367
|
+
beforeCapture: async (page: Page) => {
|
|
368
|
+
// Open a team detail, then click the "team" tab to see the CommGraph
|
|
369
|
+
const teamRow = page.locator('tr').filter({ hasText: 'README overhaul' }).first();
|
|
370
|
+
if (await teamRow.isVisible({ timeout: 5000 })) {
|
|
371
|
+
await teamRow.click();
|
|
372
|
+
await sleep(1000);
|
|
373
|
+
// Look for the "team" tab button
|
|
374
|
+
const teamTab = page.locator('button', { hasText: /^team$/i }).first();
|
|
375
|
+
if (await teamTab.isVisible({ timeout: 3000 })) {
|
|
376
|
+
await teamTab.click();
|
|
377
|
+
await sleep(1000);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'lifecycle',
|
|
384
|
+
path: '/lifecycle',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'usage',
|
|
388
|
+
path: '/usage',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'projects',
|
|
392
|
+
path: '/projects',
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
async function captureScreenshots(): Promise<void> {
|
|
397
|
+
// Ensure output directory exists
|
|
398
|
+
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
399
|
+
|
|
400
|
+
const browser: Browser = await chromium.launch({ headless: true });
|
|
401
|
+
const context = await browser.newContext({
|
|
402
|
+
viewport: VIEWPORT,
|
|
403
|
+
colorScheme: 'dark',
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
for (const spec of SCREENSHOTS) {
|
|
408
|
+
const page: Page = await context.newPage();
|
|
409
|
+
console.log(`Capturing: ${spec.name} (${spec.path})`);
|
|
410
|
+
|
|
411
|
+
await page.goto(`${BASE_URL}${spec.path}`, { waitUntil: 'networkidle' });
|
|
412
|
+
|
|
413
|
+
// Allow rendering to settle
|
|
414
|
+
await sleep(2000);
|
|
415
|
+
|
|
416
|
+
if (spec.beforeCapture) {
|
|
417
|
+
await spec.beforeCapture(page);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const outputPath = path.join(SCREENSHOT_DIR, `${spec.name}.png`);
|
|
421
|
+
await page.screenshot({ path: outputPath, fullPage: false });
|
|
422
|
+
console.log(` -> ${outputPath}`);
|
|
423
|
+
|
|
424
|
+
await page.close();
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
await browser.close();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Cleanup
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
function cleanup(): void {
|
|
436
|
+
stopServer();
|
|
437
|
+
|
|
438
|
+
// Remove temporary database files
|
|
439
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
440
|
+
const file = `${TEMP_DB}${suffix}`;
|
|
441
|
+
if (fs.existsSync(file)) {
|
|
442
|
+
fs.unlinkSync(file);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
console.log('Cleaned up temporary database');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Main
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
async function main(): Promise<void> {
|
|
453
|
+
console.log('Fleet Commander Screenshot Capture');
|
|
454
|
+
console.log('==================================\n');
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
// 1. Seed database
|
|
458
|
+
console.log('Step 1: Seeding database...');
|
|
459
|
+
await seedDatabase();
|
|
460
|
+
|
|
461
|
+
// 2. Start server
|
|
462
|
+
console.log('\nStep 2: Starting server...');
|
|
463
|
+
await startServer();
|
|
464
|
+
|
|
465
|
+
// 3. Capture screenshots
|
|
466
|
+
console.log('\nStep 3: Capturing screenshots...');
|
|
467
|
+
await captureScreenshots();
|
|
468
|
+
|
|
469
|
+
console.log('\nDone! Screenshots saved to docs/screenshots/');
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.error('\nFailed:', err);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
} finally {
|
|
474
|
+
cleanup();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
main();
|