bmad-visual 0.1.0
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/bin/bmad-visual.js +255 -0
- package/package.json +31 -0
- package/templates/.agent/workflows/analyst.md +20 -0
- package/templates/.agent/workflows/architect.md +20 -0
- package/templates/.agent/workflows/bmad-advanced-elicitation.md +5 -0
- package/templates/.agent/workflows/bmad-brainstorming.md +5 -0
- package/templates/.agent/workflows/bmad-check-implementation-readiness.md +5 -0
- package/templates/.agent/workflows/bmad-checkpoint-preview.md +5 -0
- package/templates/.agent/workflows/bmad-code-review.md +5 -0
- package/templates/.agent/workflows/bmad-correct-course.md +5 -0
- package/templates/.agent/workflows/bmad-create-architecture.md +5 -0
- package/templates/.agent/workflows/bmad-create-epics-and-stories.md +5 -0
- package/templates/.agent/workflows/bmad-create-prd.md +5 -0
- package/templates/.agent/workflows/bmad-create-story.md +5 -0
- package/templates/.agent/workflows/bmad-create-ux-design.md +5 -0
- package/templates/.agent/workflows/bmad-dev-story.md +5 -0
- package/templates/.agent/workflows/bmad-distillator.md +5 -0
- package/templates/.agent/workflows/bmad-document-project.md +5 -0
- package/templates/.agent/workflows/bmad-domain-research.md +5 -0
- package/templates/.agent/workflows/bmad-edit-prd.md +5 -0
- package/templates/.agent/workflows/bmad-editorial-review-prose.md +5 -0
- package/templates/.agent/workflows/bmad-editorial-review-structure.md +5 -0
- package/templates/.agent/workflows/bmad-generate-project-context.md +5 -0
- package/templates/.agent/workflows/bmad-help-full.md +5 -0
- package/templates/.agent/workflows/bmad-help.md +5 -0
- package/templates/.agent/workflows/bmad-index-docs.md +5 -0
- package/templates/.agent/workflows/bmad-market-research.md +5 -0
- package/templates/.agent/workflows/bmad-party-mode.md +5 -0
- package/templates/.agent/workflows/bmad-prfaq.md +5 -0
- package/templates/.agent/workflows/bmad-product-brief.md +5 -0
- package/templates/.agent/workflows/bmad-qa-generate-e2e-tests.md +5 -0
- package/templates/.agent/workflows/bmad-quick-dev.md +5 -0
- package/templates/.agent/workflows/bmad-retrospective.md +5 -0
- package/templates/.agent/workflows/bmad-review-adversarial-general.md +5 -0
- package/templates/.agent/workflows/bmad-review-edge-case-hunter.md +5 -0
- package/templates/.agent/workflows/bmad-shard-doc.md +5 -0
- package/templates/.agent/workflows/bmad-sprint-planning.md +5 -0
- package/templates/.agent/workflows/bmad-sprint-status.md +5 -0
- package/templates/.agent/workflows/bmad-technical-research.md +5 -0
- package/templates/.agent/workflows/bmad-validate-prd.md +5 -0
- package/templates/.agent/workflows/dev.md +20 -0
- package/templates/.agent/workflows/pm.md +20 -0
- package/templates/.agent/workflows/qa.md +20 -0
- package/templates/.agent/workflows/sm.md +20 -0
- package/templates/.agent/workflows/solo-dev.md +20 -0
- package/templates/.agent/workflows/ux.md +20 -0
- package/templates/.claude/settings.local.json +50 -0
- package/templates/.claude/skills/analyst/SKILL.md +77 -0
- package/templates/.claude/skills/architect/SKILL.md +72 -0
- package/templates/.claude/skills/bmad-advanced-elicitation/SKILL.md +24 -0
- package/templates/.claude/skills/bmad-brainstorming/SKILL.md +26 -0
- package/templates/.claude/skills/bmad-check-implementation-readiness/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-checkpoint-preview/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-code-review/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-correct-course/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-create-architecture/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-create-epics-and-stories/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-create-prd/SKILL.md +30 -0
- package/templates/.claude/skills/bmad-create-story/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-create-ux-design/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-dev-story/SKILL.md +27 -0
- package/templates/.claude/skills/bmad-distillator/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-document-project/SKILL.md +27 -0
- package/templates/.claude/skills/bmad-domain-research/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-edit-prd/SKILL.md +18 -0
- package/templates/.claude/skills/bmad-editorial-review-prose/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-editorial-review-structure/SKILL.md +25 -0
- package/templates/.claude/skills/bmad-generate-project-context/SKILL.md +20 -0
- package/templates/.claude/skills/bmad-help/SKILL.md +63 -0
- package/templates/.claude/skills/bmad-help-full/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-index-docs/SKILL.md +16 -0
- package/templates/.claude/skills/bmad-market-research/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-party-mode/SKILL.md +24 -0
- package/templates/.claude/skills/bmad-prfaq/SKILL.md +36 -0
- package/templates/.claude/skills/bmad-product-brief/SKILL.md +36 -0
- package/templates/.claude/skills/bmad-qa-generate-e2e-tests/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-quick-dev/SKILL.md +23 -0
- package/templates/.claude/skills/bmad-retrospective/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-review-adversarial-general/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-review-edge-case-hunter/SKILL.md +22 -0
- package/templates/.claude/skills/bmad-shard-doc/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-sprint-planning/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-sprint-status/SKILL.md +17 -0
- package/templates/.claude/skills/bmad-technical-research/SKILL.md +21 -0
- package/templates/.claude/skills/bmad-validate-prd/SKILL.md +22 -0
- package/templates/.claude/skills/dev/SKILL.md +71 -0
- package/templates/.claude/skills/pm/SKILL.md +76 -0
- package/templates/.claude/skills/qa/SKILL.md +64 -0
- package/templates/.claude/skills/sm/SKILL.md +74 -0
- package/templates/.claude/skills/solo-dev/SKILL.md +64 -0
- package/templates/.claude/skills/ux/SKILL.md +71 -0
- package/templates/CLAUDE.md +60 -0
- package/templates/dashboard/index.html +15 -0
- package/templates/dashboard/package.json +30 -0
- package/templates/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/templates/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/templates/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/templates/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/templates/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/templates/dashboard/public/assets/furniture/blinds.png +0 -0
- package/templates/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/templates/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/templates/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/templates/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/templates/dashboard/public/assets/furniture/clock.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/templates/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/templates/dashboard/public/assets/furniture/couch.png +0 -0
- package/templates/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/templates/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/templates/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/templates/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/templates/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/templates/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/templates/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/templates/dashboard/public/assets/furniture/lantern.png +0 -0
- package/templates/dashboard/public/assets/furniture/monstera.png +0 -0
- package/templates/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/templates/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant1.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant2.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant3.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/templates/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/templates/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/rug.png +0 -0
- package/templates/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/templates/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/templates/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/templates/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/templates/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/templates/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/templates/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/templates/dashboard/public/assets/logo.png +0 -0
- package/templates/dashboard/src/App.tsx +119 -0
- package/templates/dashboard/src/components/SquadCard.tsx +47 -0
- package/templates/dashboard/src/components/SquadSelector.tsx +61 -0
- package/templates/dashboard/src/components/StatusBadge.tsx +32 -0
- package/templates/dashboard/src/components/StatusBar.tsx +97 -0
- package/templates/dashboard/src/hooks/useSquadSocket.ts +137 -0
- package/templates/dashboard/src/lib/formatTime.ts +16 -0
- package/templates/dashboard/src/lib/normalizeState.ts +25 -0
- package/templates/dashboard/src/main.tsx +14 -0
- package/templates/dashboard/src/office/AgentSprite.ts +249 -0
- package/templates/dashboard/src/office/OfficeScene.ts +577 -0
- package/templates/dashboard/src/office/PhaserGame.tsx +101 -0
- package/templates/dashboard/src/office/RoomBuilder.ts +190 -0
- package/templates/dashboard/src/office/assetKeys.ts +150 -0
- package/templates/dashboard/src/office/palette.ts +32 -0
- package/templates/dashboard/src/plugin/squadWatcher.ts +260 -0
- package/templates/dashboard/src/store/useSquadStore.ts +63 -0
- package/templates/dashboard/src/styles/globals.css +41 -0
- package/templates/dashboard/src/types/state.ts +69 -0
- package/templates/dashboard/src/vite-env.d.ts +1 -0
- package/templates/dashboard/tsconfig.json +24 -0
- package/templates/dashboard/tsconfig.tsbuildinfo +1 -0
- package/templates/dashboard/vite.config.ts +13 -0
- package/templates/squads/.gitkeep +0 -0
- package/templates/squads/bmad/agents/analyst.agent.md +87 -0
- package/templates/squads/bmad/agents/architect.agent.md +82 -0
- package/templates/squads/bmad/agents/developer.agent.md +91 -0
- package/templates/squads/bmad/agents/pm.agent.md +86 -0
- package/templates/squads/bmad/agents/qa-engineer.agent.md +84 -0
- package/templates/squads/bmad/agents/scrum-master.agent.md +84 -0
- package/templates/squads/bmad/agents/solo-dev.agent.md +81 -0
- package/templates/squads/bmad/agents/tech-writer.agent.md +86 -0
- package/templates/squads/bmad/agents/ux-designer.agent.md +81 -0
- package/templates/squads/bmad/squad.yaml +70 -0
- package/templates/squads/bmad/state.json +108 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
import { COLORS, TILE, MARGIN, WALL_H } from './palette';
|
|
3
|
+
import { FURNITURE_KEYS } from './assetKeys';
|
|
4
|
+
|
|
5
|
+
export class RoomBuilder {
|
|
6
|
+
private scene: Phaser.Scene;
|
|
7
|
+
|
|
8
|
+
constructor(scene: Phaser.Scene) {
|
|
9
|
+
this.scene = scene;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
build(roomW: number, roomH: number): void {
|
|
13
|
+
this.drawFloor(roomW, roomH);
|
|
14
|
+
this.drawWalls(roomW);
|
|
15
|
+
this.drawRoomBorder(roomW, roomH);
|
|
16
|
+
this.placeFurniture(roomW, roomH);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private drawFloor(roomW: number, roomH: number): void {
|
|
20
|
+
const g = this.scene.add.graphics();
|
|
21
|
+
// Main floor fill
|
|
22
|
+
g.fillStyle(COLORS.floor, 1);
|
|
23
|
+
g.fillRect(0, WALL_H, roomW, roomH - WALL_H);
|
|
24
|
+
// Checkerboard texture
|
|
25
|
+
g.fillStyle(COLORS.floorAlt, 0.25);
|
|
26
|
+
for (let y = WALL_H; y < roomH; y += TILE) {
|
|
27
|
+
for (let x = 0; x < roomW; x += TILE) {
|
|
28
|
+
if ((x / TILE + y / TILE) % 2 === 0) {
|
|
29
|
+
g.fillRect(x, y, TILE, TILE);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Shadow along the wall base for depth
|
|
34
|
+
g.fillStyle(0x000000, 0.1);
|
|
35
|
+
g.fillRect(0, WALL_H, roomW, 10);
|
|
36
|
+
g.fillStyle(0x000000, 0.05);
|
|
37
|
+
g.fillRect(0, WALL_H + 10, roomW, 6);
|
|
38
|
+
// Subtle shadow along left and right walls
|
|
39
|
+
g.fillStyle(0x000000, 0.04);
|
|
40
|
+
g.fillRect(0, WALL_H, 6, roomH - WALL_H);
|
|
41
|
+
g.fillRect(roomW - 6, WALL_H, 6, roomH - WALL_H);
|
|
42
|
+
// Subtle warm highlight on bottom edge
|
|
43
|
+
g.fillStyle(0xddc89e, 0.15);
|
|
44
|
+
g.fillRect(0, roomH - 4, roomW, 4);
|
|
45
|
+
g.setDepth(-2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private drawWalls(roomW: number): void {
|
|
49
|
+
const g = this.scene.add.graphics();
|
|
50
|
+
// Wall background — slight gradient effect with two tones
|
|
51
|
+
g.fillStyle(COLORS.wall, 1);
|
|
52
|
+
g.fillRect(0, 0, roomW, WALL_H);
|
|
53
|
+
// Lighter upper band for depth
|
|
54
|
+
g.fillStyle(0xede2d6, 1);
|
|
55
|
+
g.fillRect(0, 0, roomW, WALL_H / 3);
|
|
56
|
+
// Baseboard trim
|
|
57
|
+
g.fillStyle(COLORS.wallTrim, 1);
|
|
58
|
+
g.fillRect(0, WALL_H - 5, roomW, 5);
|
|
59
|
+
// Thin highlight line above baseboard
|
|
60
|
+
g.fillStyle(0xc8b8a8, 1);
|
|
61
|
+
g.fillRect(0, WALL_H - 6, roomW, 1);
|
|
62
|
+
g.setDepth(-1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private drawRoomBorder(roomW: number, roomH: number): void {
|
|
66
|
+
const g = this.scene.add.graphics();
|
|
67
|
+
const bg = 0x0a0e1a;
|
|
68
|
+
const pad = 200; // generous padding to cover any viewport overflow
|
|
69
|
+
|
|
70
|
+
// Dark overlay strips outside the room on all 4 sides
|
|
71
|
+
g.fillStyle(bg, 1);
|
|
72
|
+
g.fillRect(-pad, -pad, roomW + pad * 2, pad); // top
|
|
73
|
+
g.fillRect(-pad, roomH, roomW + pad * 2, pad); // bottom
|
|
74
|
+
g.fillRect(-pad, -pad, pad, roomH + pad * 2); // left
|
|
75
|
+
g.fillRect(roomW, -pad, pad, roomH + pad * 2); // right
|
|
76
|
+
|
|
77
|
+
// Clean 2px border around room edge
|
|
78
|
+
g.lineStyle(2, 0x1a2540, 0.8);
|
|
79
|
+
g.strokeRect(0, 0, roomW, roomH);
|
|
80
|
+
g.setDepth(1000);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
placeFurniture(roomW: number, roomH: number): void {
|
|
84
|
+
const s = this.scene;
|
|
85
|
+
const centerX = roomW / 2;
|
|
86
|
+
const deskAreaBottom = roomH - MARGIN;
|
|
87
|
+
const loungeY = deskAreaBottom - TILE * 0.5;
|
|
88
|
+
|
|
89
|
+
// ================================================================
|
|
90
|
+
// WALL DECORATIONS — 5 evenly-spaced sections for breathing room
|
|
91
|
+
// ================================================================
|
|
92
|
+
const wallSections = 5;
|
|
93
|
+
const section = roomW / wallSections;
|
|
94
|
+
|
|
95
|
+
// Section 1: Window — far left
|
|
96
|
+
s.add.image(section * 0.6, WALL_H + 5, FURNITURE_KEYS.blindsLargeWhite)
|
|
97
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(0);
|
|
98
|
+
|
|
99
|
+
// Section 2: Bookshelf purple tall — left
|
|
100
|
+
s.add.image(section * 1.8, WALL_H + 40, FURNITURE_KEYS.bookshelfPurpleTall)
|
|
101
|
+
.setOrigin(0.5, 1).setScale(2).setDepth(0);
|
|
102
|
+
|
|
103
|
+
// Section 4: Whiteboard stand with graph — right of center
|
|
104
|
+
s.add.image(section * 3.2, WALL_H + 40, FURNITURE_KEYS.whiteboardStandGraph)
|
|
105
|
+
.setOrigin(0.5, 1).setScale(2.0).setDepth(0);
|
|
106
|
+
|
|
107
|
+
// Section 5: Window — far right
|
|
108
|
+
s.add.image(section * 4.4, WALL_H + 5, FURNITURE_KEYS.blindsLargeWhite)
|
|
109
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(0);
|
|
110
|
+
|
|
111
|
+
// ================================================================
|
|
112
|
+
// CORNER PLANTS — one plant anchored to each room corner
|
|
113
|
+
// ================================================================
|
|
114
|
+
// Top-left corner
|
|
115
|
+
s.add.image(MARGIN / 2 + 8, WALL_H + TILE * 2, FURNITURE_KEYS.monstera)
|
|
116
|
+
.setOrigin(0.5, 1).setScale(1.3).setDepth(WALL_H + TILE * 2);
|
|
117
|
+
|
|
118
|
+
// Top-right corner — water cooler against the right wall
|
|
119
|
+
s.add.image(roomW - MARGIN / 4, WALL_H + TILE * 2, FURNITURE_KEYS.waterCooler)
|
|
120
|
+
.setOrigin(0.5, 1).setScale(1.5).setDepth(WALL_H + TILE * 2 + 1);
|
|
121
|
+
|
|
122
|
+
// Bottom-left corner
|
|
123
|
+
s.add.image(MARGIN / 2, roomH - TILE * 0.5, FURNITURE_KEYS.plant3)
|
|
124
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(roomH - TILE * 0.5);
|
|
125
|
+
|
|
126
|
+
// Bottom-right corner
|
|
127
|
+
s.add.image(roomW - MARGIN / 2.5, roomH - TILE * 0.5, FURNITURE_KEYS.plantPoof)
|
|
128
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(roomH - TILE * 0.5);
|
|
129
|
+
|
|
130
|
+
// Bottom-right corner — treasure chest
|
|
131
|
+
s.add.image(roomW - MARGIN / 4, roomH - TILE * 2, FURNITURE_KEYS.treasurechestGold)
|
|
132
|
+
.setOrigin(0.5, 1).setScale(1.4).setDepth(1);
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// Middle-left and middle-right (vertical center)
|
|
138
|
+
const midY = WALL_H + (roomH - WALL_H) / 2;
|
|
139
|
+
s.add.image(MARGIN / 2, midY, FURNITURE_KEYS.monsteraSmall)
|
|
140
|
+
.setOrigin(0.5, 1).setScale(1.5).setDepth(midY);
|
|
141
|
+
s.add.image(roomW - MARGIN / 2, midY, FURNITURE_KEYS.plant1)
|
|
142
|
+
.setOrigin(0.5, 1).setScale(1.5).setDepth(midY);
|
|
143
|
+
|
|
144
|
+
// ================================================================
|
|
145
|
+
// DESK-AREA ACCENTS — flowers near wall, anchored to desk tops
|
|
146
|
+
// ================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
// ================================================================
|
|
150
|
+
// LOUNGE ZONE — cozy social area at bottom
|
|
151
|
+
// ================================================================
|
|
152
|
+
// Rug — wide rectangular, covers full lounge seating group (448x256 sprite)
|
|
153
|
+
s.add.image(centerX, loungeY + TILE * 0.9, FURNITURE_KEYS.fancyRugWide)
|
|
154
|
+
.setOrigin(0.5, 0.5).setScale(0.7).setDepth(-0.5);
|
|
155
|
+
|
|
156
|
+
// Couch — centered, bigger
|
|
157
|
+
const couchY = loungeY + TILE * 0.3;
|
|
158
|
+
s.add.image(centerX, couchY, FURNITURE_KEYS.couchTanDown)
|
|
159
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(couchY);
|
|
160
|
+
|
|
161
|
+
// Cushions — sitting on the couch seat (slightly above couch bottom)
|
|
162
|
+
s.add.image(centerX - 26, couchY - 28, FURNITURE_KEYS.cushionBlue)
|
|
163
|
+
.setOrigin(0.5, 0.5).setScale(1.5).setDepth(couchY + 1);
|
|
164
|
+
s.add.image(centerX + 26, couchY - 28, FURNITURE_KEYS.cushionTan)
|
|
165
|
+
.setOrigin(0.5, 0.5).setScale(1.5).setDepth(couchY + 1);
|
|
166
|
+
|
|
167
|
+
// Armchairs — flanking the couch, same scale
|
|
168
|
+
s.add.image(centerX - 95, loungeY + TILE * 0.8, FURNITURE_KEYS.armchairTanDown)
|
|
169
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(loungeY + TILE * 0.8);
|
|
170
|
+
s.add.image(centerX + 95, loungeY + TILE * 0.8, FURNITURE_KEYS.armchairTanDown)
|
|
171
|
+
.setOrigin(0.5, 1).setScale(1.8).setDepth(loungeY + TILE * 0.8);
|
|
172
|
+
|
|
173
|
+
// Coffee table — in front of couch
|
|
174
|
+
s.add.image(centerX, loungeY + TILE * 1.5, FURNITURE_KEYS.coffeeTable)
|
|
175
|
+
.setOrigin(0.5, 1).setScale(1.4).setDepth(loungeY + TILE * 1.5);
|
|
176
|
+
|
|
177
|
+
// Coffee mug on table
|
|
178
|
+
s.add.image(centerX + 12, loungeY + TILE * 1.3, FURNITURE_KEYS.coffeeMugBlue)
|
|
179
|
+
.setOrigin(0.5, 1).setDepth(loungeY + TILE * 1.5 + 1);
|
|
180
|
+
|
|
181
|
+
// Coffee station — right side of lounge
|
|
182
|
+
const stationX = centerX + TILE * 6;
|
|
183
|
+
const stationY = loungeY + TILE * 1.9;
|
|
184
|
+
s.add.image(stationX, stationY, FURNITURE_KEYS.coffeetableBlackH)
|
|
185
|
+
.setOrigin(0.5, 1).setScale(1.5).setDepth(stationY);
|
|
186
|
+
s.add.image(stationX, stationY + 5, FURNITURE_KEYS.coffeepotRight)
|
|
187
|
+
.setOrigin(0.5, 1).setScale(1.2).setDepth(stationY + 1);
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Asset key constants for Phaser loader
|
|
2
|
+
// All paths relative to /assets/ in public/
|
|
3
|
+
|
|
4
|
+
// --- Characters ---
|
|
5
|
+
export const MALE_CHARACTERS = ['Male1', 'Male2', 'Male3', 'Male4'] as const;
|
|
6
|
+
export const FEMALE_CHARACTERS = ['Female1', 'Female2', 'Female3', 'Female4', 'Female5', 'Female6'] as const;
|
|
7
|
+
|
|
8
|
+
export type CharacterName =
|
|
9
|
+
| typeof MALE_CHARACTERS[number]
|
|
10
|
+
| typeof FEMALE_CHARACTERS[number];
|
|
11
|
+
|
|
12
|
+
// Combined array for preloading all sprites
|
|
13
|
+
export const CHARACTER_NAMES = [...MALE_CHARACTERS, ...FEMALE_CHARACTERS] as const;
|
|
14
|
+
|
|
15
|
+
// Characters with only a single wave frame (no _1wave/_2wave variants)
|
|
16
|
+
const SINGLE_WAVE = new Set<CharacterName>(['Male3', 'Male4', 'Female3', 'Female4', 'Female5', 'Female6']);
|
|
17
|
+
|
|
18
|
+
// Returns the asset keys for a character's animation frames
|
|
19
|
+
export function avatarKeys(name: CharacterName) {
|
|
20
|
+
return {
|
|
21
|
+
blink: `avatar_${name}_blink`,
|
|
22
|
+
talk: `avatar_${name}_talk`,
|
|
23
|
+
wave1: `avatar_${name}_wave1`,
|
|
24
|
+
wave2: `avatar_${name}_wave2`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function avatarPath(name: CharacterName, pose: string): string {
|
|
29
|
+
if (pose === 'wave1') return SINGLE_WAVE.has(name) ? `assets/avatars/${name}_wave.png` : `assets/avatars/${name}_1wave.png`;
|
|
30
|
+
if (pose === 'wave2') return SINGLE_WAVE.has(name) ? `assets/avatars/${name}_wave.png` : `assets/avatars/${name}_2wave.png`;
|
|
31
|
+
return `assets/avatars/${name}_${pose}.png`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Desks ---
|
|
35
|
+
export const DESK_KEYS = {
|
|
36
|
+
blackIdle: 'desk_black_idle',
|
|
37
|
+
blackCoding: 'desk_black_coding',
|
|
38
|
+
blackCodingAlt: 'desk_black_coding_alt',
|
|
39
|
+
whiteIdle: 'desk_white_idle',
|
|
40
|
+
whiteCoding: 'desk_white_coding',
|
|
41
|
+
whiteCodingAlt: 'desk_white_coding_alt',
|
|
42
|
+
blackUp: 'desk_black_up',
|
|
43
|
+
whiteUp: 'desk_white_up',
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export const DESK_PATHS: Record<string, string> = {
|
|
47
|
+
[DESK_KEYS.blackIdle]: 'assets/desks/desktop_set_black_down.png',
|
|
48
|
+
[DESK_KEYS.blackCoding]: 'assets/desks/desktop_set_black_down_coding.png',
|
|
49
|
+
[DESK_KEYS.blackCodingAlt]: 'assets/desks/desktop_set_black_down_coding-1.png',
|
|
50
|
+
[DESK_KEYS.whiteIdle]: 'assets/desks/desktop_set_white_down.png',
|
|
51
|
+
[DESK_KEYS.whiteCoding]: 'assets/desks/desktop_set_white_down_coding.png',
|
|
52
|
+
[DESK_KEYS.whiteCodingAlt]: 'assets/desks/desktop_set_white_down_coding-1.png',
|
|
53
|
+
[DESK_KEYS.blackUp]: 'assets/desks/desktop_set_black_up.png',
|
|
54
|
+
[DESK_KEYS.whiteUp]: 'assets/desks/desktop_set_white_up.png',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// --- Furniture ---
|
|
58
|
+
export const FURNITURE_KEYS = {
|
|
59
|
+
bookshelf: 'furniture_bookshelf',
|
|
60
|
+
whiteboard: 'furniture_whiteboard',
|
|
61
|
+
clock: 'furniture_clock',
|
|
62
|
+
plant1: 'furniture_plant1',
|
|
63
|
+
plant2: 'furniture_plant2',
|
|
64
|
+
plant3: 'furniture_plant3',
|
|
65
|
+
flowers1: 'furniture_flowers1',
|
|
66
|
+
flowers2: 'furniture_flowers2',
|
|
67
|
+
couch: 'furniture_couch',
|
|
68
|
+
rug: 'furniture_rug',
|
|
69
|
+
coffeeMug: 'furniture_coffee_mug',
|
|
70
|
+
blinds: 'furniture_blinds',
|
|
71
|
+
coffeeTable: 'furniture_coffee_table',
|
|
72
|
+
// New assets
|
|
73
|
+
lampTan: 'furniture_lamp_tan',
|
|
74
|
+
monstera: 'furniture_monstera',
|
|
75
|
+
monsteraSmall: 'furniture_monstera_small',
|
|
76
|
+
succulentGreen: 'furniture_succulent_green',
|
|
77
|
+
succulentBlue: 'furniture_succulent_blue',
|
|
78
|
+
posterBlue: 'furniture_poster_blue',
|
|
79
|
+
bulletinBoard: 'furniture_bulletin_board',
|
|
80
|
+
fancyRug: 'furniture_fancy_rug',
|
|
81
|
+
cushionBlue: 'furniture_cushion_blue',
|
|
82
|
+
cushionTan: 'furniture_cushion_tan',
|
|
83
|
+
armchairTan: 'furniture_armchair_tan',
|
|
84
|
+
backpackBlue: 'furniture_backpack_blue',
|
|
85
|
+
backpackRed: 'furniture_backpack_red',
|
|
86
|
+
plantPoof: 'furniture_plant_poof',
|
|
87
|
+
plantSpindly: 'furniture_plant_spindly',
|
|
88
|
+
coffeeMugBlue: 'furniture_coffee_mug_blue',
|
|
89
|
+
pictureFrame: 'furniture_picture_frame',
|
|
90
|
+
lantern: 'furniture_lantern',
|
|
91
|
+
windowBlindsOpen: 'furniture_window_blinds_open',
|
|
92
|
+
couchTanDown: 'furniture_couch_tan_down',
|
|
93
|
+
armchairTanDown: 'furniture_armchair_tan_down',
|
|
94
|
+
deskWood: 'furniture_desk_wood',
|
|
95
|
+
fancyRugWide: 'furniture_fancy_rug_wide',
|
|
96
|
+
waterCooler: 'furniture_water_cooler',
|
|
97
|
+
whiteboardStandGraph: 'furniture_whiteboard_stand_graph',
|
|
98
|
+
bookshelfPurpleTall: 'furniture_bookshelf_purple_tall',
|
|
99
|
+
coffeetableBlackH: 'furniture_coffeetable_black_h',
|
|
100
|
+
coffeepotRight: 'furniture_coffeepot_right',
|
|
101
|
+
blindsLargeWhite: 'furniture_blinds_large_white',
|
|
102
|
+
treasurechestGold: 'furniture_treasurechest_gold',
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
export const FURNITURE_PATHS: Record<string, string> = {
|
|
106
|
+
[FURNITURE_KEYS.bookshelf]: 'assets/furniture/bookshelf.png',
|
|
107
|
+
[FURNITURE_KEYS.whiteboard]: 'assets/furniture/whiteboard.png',
|
|
108
|
+
[FURNITURE_KEYS.clock]: 'assets/furniture/clock.png',
|
|
109
|
+
[FURNITURE_KEYS.plant1]: 'assets/furniture/plant1.png',
|
|
110
|
+
[FURNITURE_KEYS.plant2]: 'assets/furniture/plant2.png',
|
|
111
|
+
[FURNITURE_KEYS.plant3]: 'assets/furniture/plant3.png',
|
|
112
|
+
[FURNITURE_KEYS.flowers1]: 'assets/furniture/flowers1.png',
|
|
113
|
+
[FURNITURE_KEYS.flowers2]: 'assets/furniture/flowers2.png',
|
|
114
|
+
[FURNITURE_KEYS.couch]: 'assets/furniture/couch.png',
|
|
115
|
+
[FURNITURE_KEYS.rug]: 'assets/furniture/rug.png',
|
|
116
|
+
[FURNITURE_KEYS.coffeeMug]: 'assets/furniture/coffee_mug.png',
|
|
117
|
+
[FURNITURE_KEYS.blinds]: 'assets/furniture/blinds.png',
|
|
118
|
+
[FURNITURE_KEYS.coffeeTable]: 'assets/furniture/coffee_table.png',
|
|
119
|
+
// New assets
|
|
120
|
+
[FURNITURE_KEYS.lampTan]: 'assets/furniture/lamp_tan.png',
|
|
121
|
+
[FURNITURE_KEYS.monstera]: 'assets/furniture/monstera.png',
|
|
122
|
+
[FURNITURE_KEYS.monsteraSmall]: 'assets/furniture/monstera_small.png',
|
|
123
|
+
[FURNITURE_KEYS.succulentGreen]: 'assets/furniture/succulent_green.png',
|
|
124
|
+
[FURNITURE_KEYS.succulentBlue]: 'assets/furniture/succulent_blue.png',
|
|
125
|
+
[FURNITURE_KEYS.posterBlue]: 'assets/furniture/poster_blue.png',
|
|
126
|
+
[FURNITURE_KEYS.bulletinBoard]: 'assets/furniture/bulletin_board.png',
|
|
127
|
+
[FURNITURE_KEYS.fancyRug]: 'assets/furniture/fancy_rug.png',
|
|
128
|
+
[FURNITURE_KEYS.cushionBlue]: 'assets/furniture/cushion_blue.png',
|
|
129
|
+
[FURNITURE_KEYS.cushionTan]: 'assets/furniture/cushion_tan.png',
|
|
130
|
+
[FURNITURE_KEYS.armchairTan]: 'assets/furniture/armchair_tan.png',
|
|
131
|
+
[FURNITURE_KEYS.backpackBlue]: 'assets/furniture/backpack_blue.png',
|
|
132
|
+
[FURNITURE_KEYS.backpackRed]: 'assets/furniture/backpack_red.png',
|
|
133
|
+
[FURNITURE_KEYS.plantPoof]: 'assets/furniture/plant_poof.png',
|
|
134
|
+
[FURNITURE_KEYS.plantSpindly]: 'assets/furniture/plant_spindly.png',
|
|
135
|
+
[FURNITURE_KEYS.coffeeMugBlue]: 'assets/furniture/coffee_mug_blue.png',
|
|
136
|
+
[FURNITURE_KEYS.pictureFrame]: 'assets/furniture/picture_frame.png',
|
|
137
|
+
[FURNITURE_KEYS.lantern]: 'assets/furniture/lantern.png',
|
|
138
|
+
[FURNITURE_KEYS.windowBlindsOpen]: 'assets/furniture/window_blinds_open.png',
|
|
139
|
+
[FURNITURE_KEYS.couchTanDown]: 'assets/furniture/couch_tan_down.png',
|
|
140
|
+
[FURNITURE_KEYS.armchairTanDown]: 'assets/furniture/armchair_tan_down.png',
|
|
141
|
+
[FURNITURE_KEYS.deskWood]: 'assets/furniture/desk_wood.png',
|
|
142
|
+
[FURNITURE_KEYS.fancyRugWide]: 'assets/furniture/fancy_rug_wide.png',
|
|
143
|
+
[FURNITURE_KEYS.waterCooler]: 'assets/furniture/water_cooler_better.png',
|
|
144
|
+
[FURNITURE_KEYS.whiteboardStandGraph]: 'assets/furniture/whiteboard_stand_graph.png',
|
|
145
|
+
[FURNITURE_KEYS.bookshelfPurpleTall]: 'assets/furniture/bookshelf_purple_tall.png',
|
|
146
|
+
[FURNITURE_KEYS.coffeetableBlackH]: 'assets/furniture/coffeetable_black_horizontal.png',
|
|
147
|
+
[FURNITURE_KEYS.coffeepotRight]: 'assets/furniture/coffeepot_right.png',
|
|
148
|
+
[FURNITURE_KEYS.blindsLargeWhite]: 'assets/furniture/blinds_large_closed_white.png',
|
|
149
|
+
[FURNITURE_KEYS.treasurechestGold]: 'assets/furniture/treasurechest_closed_gold.png',
|
|
150
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Slimmed palette — only badge colors and layout constants
|
|
2
|
+
// All visual rendering now uses sprite assets, not procedural colors
|
|
3
|
+
|
|
4
|
+
export const COLORS = {
|
|
5
|
+
// Status badge dots
|
|
6
|
+
statusIdle: 0x6b7894,
|
|
7
|
+
statusWorking: 0x3b82f6,
|
|
8
|
+
statusDone: 0x10b981,
|
|
9
|
+
statusCheckpoint: 0xf59e0b,
|
|
10
|
+
|
|
11
|
+
// Name badge
|
|
12
|
+
nameCardBg: 0x0f1629,
|
|
13
|
+
nameCardText: 0xf0f0f0,
|
|
14
|
+
|
|
15
|
+
// Background
|
|
16
|
+
background: 0x0a0e1a,
|
|
17
|
+
|
|
18
|
+
// Floor fill (warm wood)
|
|
19
|
+
floor: 0xc8ac86,
|
|
20
|
+
floorAlt: 0xbca07a,
|
|
21
|
+
|
|
22
|
+
// Wall fill
|
|
23
|
+
wall: 0xe6dace,
|
|
24
|
+
wallTrim: 0xa89888,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
// Layout constants
|
|
28
|
+
export const TILE = 32; // Base tile size in pixels
|
|
29
|
+
export const CELL_W = 3 * TILE; // 96px — desk cell width (tighter grid)
|
|
30
|
+
export const CELL_H = 3 * TILE; // 96px — desk cell height
|
|
31
|
+
export const MARGIN = 3 * TILE; // 96px — room edge margin (more breathing room)
|
|
32
|
+
export const WALL_H = 3 * TILE; // 96px — wall strip height (taller for decorations)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { Plugin, ViteDevServer } from "vite";
|
|
2
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
+
import type { Server, IncomingMessage } from "node:http";
|
|
4
|
+
import type { Duplex } from "node:stream";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import fsp from "node:fs/promises";
|
|
7
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import type { SquadInfo, SquadState, WsMessage } from "../types/state";
|
|
11
|
+
|
|
12
|
+
function resolveSquadsDir(): string {
|
|
13
|
+
const candidates = [
|
|
14
|
+
path.resolve(process.cwd(), "../squads"), // started from dashboard/
|
|
15
|
+
path.resolve(process.cwd(), "squads"), // started from project root
|
|
16
|
+
];
|
|
17
|
+
for (const c of candidates) {
|
|
18
|
+
if (fs.existsSync(c)) return c;
|
|
19
|
+
}
|
|
20
|
+
return path.resolve(process.cwd(), "../squads"); // default (will be created on demand)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function discoverSquads(squadsDir: string): Promise<SquadInfo[]> {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await fsp.readdir(squadsDir, { withFileTypes: true });
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const squads: SquadInfo[] = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory()) continue;
|
|
35
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
36
|
+
|
|
37
|
+
const yamlPath = path.join(squadsDir, entry.name, "squad.yaml");
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fsp.readFile(yamlPath, "utf-8");
|
|
40
|
+
const parsed = parseYaml(raw);
|
|
41
|
+
const s = parsed?.squad;
|
|
42
|
+
if (s) {
|
|
43
|
+
squads.push({
|
|
44
|
+
code: typeof s.code === "string" ? s.code : entry.name,
|
|
45
|
+
name: typeof s.name === "string" ? s.name : entry.name,
|
|
46
|
+
description: typeof s.description === "string" ? s.description : "",
|
|
47
|
+
icon: typeof s.icon === "string" ? s.icon : "\u{1F4CB}",
|
|
48
|
+
agents: Array.isArray(s.agents) ? (s.agents as unknown[]).filter((a): a is string => typeof a === "string") : [],
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// No squad.yaml or invalid YAML — fall through to default
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
squads.push({
|
|
57
|
+
code: entry.name,
|
|
58
|
+
name: entry.name,
|
|
59
|
+
description: "",
|
|
60
|
+
icon: "\u{1F4CB}",
|
|
61
|
+
agents: [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return squads;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isValidState(data: unknown): data is SquadState {
|
|
69
|
+
if (!data || typeof data !== "object") return false;
|
|
70
|
+
const d = data as Record<string, unknown>;
|
|
71
|
+
return (
|
|
72
|
+
typeof d.status === "string" &&
|
|
73
|
+
d.step != null && typeof d.step === "object" &&
|
|
74
|
+
Array.isArray(d.agents)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readActiveStates(squadsDir: string): Promise<Record<string, SquadState>> {
|
|
79
|
+
const states: Record<string, SquadState> = {};
|
|
80
|
+
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await fsp.readdir(squadsDir, { withFileTypes: true });
|
|
84
|
+
} catch {
|
|
85
|
+
return states;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory()) continue;
|
|
90
|
+
const statePath = path.join(squadsDir, entry.name, "state.json");
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const raw = await fsp.readFile(statePath, "utf-8");
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (isValidState(parsed)) {
|
|
96
|
+
states[entry.name] = parsed;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Skip missing or invalid JSON
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return states;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function buildSnapshot(squadsDir: string): Promise<WsMessage> {
|
|
107
|
+
return {
|
|
108
|
+
type: "SNAPSHOT",
|
|
109
|
+
squads: await discoverSquads(squadsDir),
|
|
110
|
+
activeStates: await readActiveStates(squadsDir),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function broadcast(wss: WebSocketServer, msg: WsMessage) {
|
|
115
|
+
const data = JSON.stringify(msg);
|
|
116
|
+
for (const client of wss.clients) {
|
|
117
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
118
|
+
try {
|
|
119
|
+
client.send(data);
|
|
120
|
+
} catch {
|
|
121
|
+
// Client connection dying — ws library will clean it up
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function squadWatcherPlugin(): Plugin {
|
|
128
|
+
return {
|
|
129
|
+
name: "squad-watcher",
|
|
130
|
+
configureServer(server: ViteDevServer) {
|
|
131
|
+
if (!server.httpServer) {
|
|
132
|
+
server.config.logger.warn("[squad-watcher] no httpServer — skipping");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const squadsDir = resolveSquadsDir();
|
|
137
|
+
server.config.logger.info(`[squad-watcher] squads dir: ${squadsDir}`);
|
|
138
|
+
|
|
139
|
+
// Create WebSocket server with noServer to avoid intercepting Vite's HMR
|
|
140
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
141
|
+
(server.httpServer as Server).on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
142
|
+
if (req.url === "/__squads_ws") {
|
|
143
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
144
|
+
wss.emit("connection", ws, req);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Let Vite handle all other upgrade requests (HMR)
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Send snapshot on new connection
|
|
151
|
+
wss.on("connection", async (ws) => {
|
|
152
|
+
try {
|
|
153
|
+
const snap = await buildSnapshot(squadsDir);
|
|
154
|
+
ws.send(JSON.stringify(snap));
|
|
155
|
+
} catch {
|
|
156
|
+
// Connection may have closed before snapshot was ready
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Ensure squads directory exists
|
|
161
|
+
fsp.mkdir(squadsDir, { recursive: true }).catch((err) => {
|
|
162
|
+
server.config.logger.error(`[squad-watcher] failed to create squads dir: ${err.message}`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// REST API fallback — serves snapshot over HTTP for polling clients
|
|
166
|
+
server.middlewares.use(async (req, res, next) => {
|
|
167
|
+
if (req.url !== "/api/snapshot") return next();
|
|
168
|
+
try {
|
|
169
|
+
const snapshot = await buildSnapshot(squadsDir);
|
|
170
|
+
res.setHeader("Content-Type", "application/json");
|
|
171
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
172
|
+
res.end(JSON.stringify(snapshot));
|
|
173
|
+
} catch {
|
|
174
|
+
res.writeHead(500);
|
|
175
|
+
res.end("Internal Server Error");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// REST API — reset agent status to idle after delivery animation
|
|
180
|
+
server.middlewares.use(async (req, res, next) => {
|
|
181
|
+
if (req.url !== "/api/reset-agent" || req.method !== "POST") return next();
|
|
182
|
+
let body = "";
|
|
183
|
+
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
184
|
+
req.on("end", async () => {
|
|
185
|
+
try {
|
|
186
|
+
const { squad, agentId } = JSON.parse(body);
|
|
187
|
+
const statePath = path.join(squadsDir, squad, "state.json");
|
|
188
|
+
const raw = await fsp.readFile(statePath, "utf-8");
|
|
189
|
+
const state = JSON.parse(raw);
|
|
190
|
+
if (!isValidState(state)) { res.writeHead(400); res.end("Invalid state"); return; }
|
|
191
|
+
const agent = state.agents.find((a: { id: string }) => a.id === agentId);
|
|
192
|
+
if (agent) {
|
|
193
|
+
agent.status = "idle";
|
|
194
|
+
state.updatedAt = new Date().toISOString();
|
|
195
|
+
await fsp.writeFile(statePath, JSON.stringify(state, null, 2));
|
|
196
|
+
}
|
|
197
|
+
res.setHeader("Content-Type", "application/json");
|
|
198
|
+
res.end(JSON.stringify({ ok: true }));
|
|
199
|
+
} catch {
|
|
200
|
+
res.writeHead(500);
|
|
201
|
+
res.end("Error");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// File watcher using chokidar — reliable cross-platform, handles partial writes
|
|
207
|
+
const watcher = chokidarWatch(squadsDir, {
|
|
208
|
+
ignoreInitial: true,
|
|
209
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 50 },
|
|
210
|
+
ignored: [/(^|[/\\])\./, /node_modules/, /output[/\\]/],
|
|
211
|
+
depth: 2,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
function handleFileChange(filePath: string) {
|
|
215
|
+
const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
|
|
216
|
+
const parts = relative.split("/");
|
|
217
|
+
if (parts.length < 2) return;
|
|
218
|
+
|
|
219
|
+
const squadName = parts[0];
|
|
220
|
+
const fileName = parts[1];
|
|
221
|
+
|
|
222
|
+
if (fileName === "state.json") {
|
|
223
|
+
fsp.readFile(filePath, "utf-8").then((raw) => {
|
|
224
|
+
const parsed = JSON.parse(raw);
|
|
225
|
+
if (!isValidState(parsed)) return;
|
|
226
|
+
broadcast(wss, { type: "SQUAD_UPDATE", squad: squadName, state: parsed });
|
|
227
|
+
}).catch(() => {
|
|
228
|
+
// Invalid JSON — next change event will retry
|
|
229
|
+
});
|
|
230
|
+
} else if (fileName === "squad.yaml") {
|
|
231
|
+
buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleFileRemoval(filePath: string) {
|
|
236
|
+
const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
|
|
237
|
+
const parts = relative.split("/");
|
|
238
|
+
if (parts.length < 2) return;
|
|
239
|
+
|
|
240
|
+
const squadName = parts[0];
|
|
241
|
+
const fileName = parts[1];
|
|
242
|
+
|
|
243
|
+
if (fileName === "state.json") {
|
|
244
|
+
broadcast(wss, { type: "SQUAD_INACTIVE", squad: squadName });
|
|
245
|
+
} else if (fileName === "squad.yaml") {
|
|
246
|
+
buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
watcher.on("add", handleFileChange);
|
|
251
|
+
watcher.on("change", handleFileChange);
|
|
252
|
+
watcher.on("unlink", handleFileRemoval);
|
|
253
|
+
|
|
254
|
+
server.httpServer.on("close", () => {
|
|
255
|
+
watcher.close();
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|