goiabaseeds 1.0.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/README.md +173 -0
- package/bin/goiabaseeds.js +98 -0
- package/eslint.config.js +14 -0
- package/package.json +61 -0
- package/skills/README.md +60 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/goiabaseeds-agent-creator/SKILL.md +192 -0
- package/skills/goiabaseeds-skill-creator/SKILL.md +407 -0
- package/skills/goiabaseeds-skill-creator/agents/analyzer.md +274 -0
- package/skills/goiabaseeds-skill-creator/agents/comparator.md +202 -0
- package/skills/goiabaseeds-skill-creator/agents/grader.md +223 -0
- package/skills/goiabaseeds-skill-creator/assets/eval_review.html +146 -0
- package/skills/goiabaseeds-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/goiabaseeds-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/goiabaseeds-skill-creator/references/schemas.md +430 -0
- package/skills/goiabaseeds-skill-creator/references/skill-format.md +235 -0
- package/skills/goiabaseeds-skill-creator/scripts/__init__.py +0 -0
- package/skills/goiabaseeds-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/goiabaseeds-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/goiabaseeds-skill-creator/scripts/run_eval.py +310 -0
- package/skills/goiabaseeds-skill-creator/scripts/utils.py +47 -0
- package/skills/image-creator/SKILL.md +155 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/image-generator/SKILL.md +124 -0
- package/skills/image-generator/scripts/generate.py +175 -0
- package/skills/instagram-publisher/SKILL.md +118 -0
- package/skills/instagram-publisher/scripts/publish.js +164 -0
- package/src/agent-session.js +110 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/bundle-detector.js +75 -0
- package/src/bundle.js +286 -0
- package/src/context.js +142 -0
- package/src/export.js +52 -0
- package/src/i18n.js +48 -0
- package/src/init.js +367 -0
- package/src/locales/en.json +72 -0
- package/src/locales/es.json +71 -0
- package/src/locales/pt-BR.json +71 -0
- package/src/logger.js +38 -0
- package/src/models-cli.js +165 -0
- package/src/pipeline-runner.js +478 -0
- package/src/prompt.js +46 -0
- package/src/provider.js +156 -0
- package/src/readme/README.md +181 -0
- package/src/run.js +100 -0
- package/src/runs.js +90 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/state-manager.js +280 -0
- package/src/tools.js +158 -0
- package/src/update.js +140 -0
- package/templates/_goiabaseeds/.goiabaseeds-version +1 -0
- package/templates/_goiabaseeds/_investigations/.gitkeep +0 -0
- package/templates/_goiabaseeds/config/playwright.config.json +11 -0
- package/templates/_goiabaseeds/core/architect.agent.yaml +1141 -0
- package/templates/_goiabaseeds/core/best-practices/_catalog.yaml +116 -0
- package/templates/_goiabaseeds/core/best-practices/blog-post.md +132 -0
- package/templates/_goiabaseeds/core/best-practices/blog-seo.md +127 -0
- package/templates/_goiabaseeds/core/best-practices/copywriting.md +428 -0
- package/templates/_goiabaseeds/core/best-practices/data-analysis.md +401 -0
- package/templates/_goiabaseeds/core/best-practices/email-newsletter.md +118 -0
- package/templates/_goiabaseeds/core/best-practices/email-sales.md +110 -0
- package/templates/_goiabaseeds/core/best-practices/image-design.md +349 -0
- package/templates/_goiabaseeds/core/best-practices/instagram-feed.md +235 -0
- package/templates/_goiabaseeds/core/best-practices/instagram-reels.md +112 -0
- package/templates/_goiabaseeds/core/best-practices/instagram-stories.md +107 -0
- package/templates/_goiabaseeds/core/best-practices/linkedin-article.md +116 -0
- package/templates/_goiabaseeds/core/best-practices/linkedin-post.md +121 -0
- package/templates/_goiabaseeds/core/best-practices/researching.md +347 -0
- package/templates/_goiabaseeds/core/best-practices/review.md +269 -0
- package/templates/_goiabaseeds/core/best-practices/social-networks-publishing.md +294 -0
- package/templates/_goiabaseeds/core/best-practices/strategist.md +344 -0
- package/templates/_goiabaseeds/core/best-practices/technical-writing.md +363 -0
- package/templates/_goiabaseeds/core/best-practices/twitter-post.md +105 -0
- package/templates/_goiabaseeds/core/best-practices/twitter-thread.md +122 -0
- package/templates/_goiabaseeds/core/best-practices/whatsapp-broadcast.md +107 -0
- package/templates/_goiabaseeds/core/best-practices/youtube-script.md +122 -0
- package/templates/_goiabaseeds/core/best-practices/youtube-shorts.md +112 -0
- package/templates/_goiabaseeds/core/prompts/auguste.dupin.prompt.md +1008 -0
- package/templates/_goiabaseeds/core/runner.pipeline.md +467 -0
- package/templates/_goiabaseeds/core/skills.engine.md +381 -0
- package/templates/dashboard/index.html +12 -0
- package/templates/dashboard/package-lock.json +2082 -0
- package/templates/dashboard/package.json +28 -0
- package/templates/dashboard/src/App.tsx +46 -0
- package/templates/dashboard/src/components/DepartmentCard.tsx +47 -0
- package/templates/dashboard/src/components/DepartmentSelector.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/useDepartmentSocket.ts +84 -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 +10 -0
- package/templates/dashboard/src/office/AgentDesk.tsx +151 -0
- package/templates/dashboard/src/office/HandoffEnvelope.tsx +108 -0
- package/templates/dashboard/src/office/OfficeScene.tsx +147 -0
- package/templates/dashboard/src/office/drawDesk.ts +263 -0
- package/templates/dashboard/src/office/drawFurniture.ts +129 -0
- package/templates/dashboard/src/office/drawRoom.ts +51 -0
- package/templates/dashboard/src/office/palette.ts +181 -0
- package/templates/dashboard/src/office/textures.ts +254 -0
- package/templates/dashboard/src/plugin/departmentWatcher.ts +210 -0
- package/templates/dashboard/src/store/useDepartmentStore.ts +56 -0
- package/templates/dashboard/src/styles/globals.css +36 -0
- package/templates/dashboard/src/types/state.ts +64 -0
- package/templates/dashboard/src/vite-env.d.ts +1 -0
- package/templates/dashboard/tsconfig.json +24 -0
- package/templates/dashboard/vite.config.ts +13 -0
- package/templates/departments/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/goiabaseeds.md +55 -0
- package/templates/ide-templates/antigravity/.agent/workflows/goiabaseeds.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/goiabaseeds/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +43 -0
- package/templates/ide-templates/codex/.agents/skills/goiabaseeds/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +105 -0
- package/templates/ide-templates/cursor/.cursor/commands/goiabaseeds.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/goiabaseeds.mdc +48 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/opencode/.opencode/commands/goiabaseeds.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +105 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/goiabaseeds.prompt.md +201 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// === Office Color Palette (Gather.town-inspired modern office) ===
|
|
2
|
+
export const COLORS = {
|
|
3
|
+
// Floor (wood planks)
|
|
4
|
+
woodLight: 0x9a7a56,
|
|
5
|
+
woodBase: 0x876a48,
|
|
6
|
+
woodDark: 0x755a3a,
|
|
7
|
+
woodGap: 0x4e3a28,
|
|
8
|
+
|
|
9
|
+
// Walls (clean cream)
|
|
10
|
+
wallFace: 0xe4d8cc,
|
|
11
|
+
wallTrim: 0xa89888,
|
|
12
|
+
wallShadow: 0x887868,
|
|
13
|
+
|
|
14
|
+
// Desk / Workstation (light maple)
|
|
15
|
+
deskTop: 0xd4bf9c,
|
|
16
|
+
deskEdge: 0xb8a480,
|
|
17
|
+
deskShadow: 0x806844,
|
|
18
|
+
monitorFrame: 0x2a2a32,
|
|
19
|
+
monitorScreen: 0x1a2a3a,
|
|
20
|
+
monitorScreenOn: 0x4a9aff,
|
|
21
|
+
keyboard: 0x3a3a42,
|
|
22
|
+
|
|
23
|
+
// Office chair (top-down)
|
|
24
|
+
chairSeat: 0x3a3a4a,
|
|
25
|
+
chairBase: 0x4a4a5a,
|
|
26
|
+
|
|
27
|
+
// Furniture / Decor
|
|
28
|
+
bookshelfWood: 0xc4a070,
|
|
29
|
+
plantGreen: 0x5aaa5a,
|
|
30
|
+
plantDark: 0x3a7a3a,
|
|
31
|
+
plantPot: 0xd4a878,
|
|
32
|
+
whiteboardBg: 0xf5f0ea,
|
|
33
|
+
whiteboardFrame: 0x8a8a92,
|
|
34
|
+
clockFace: 0xf0ebe0,
|
|
35
|
+
clockFrame: 0x6a6a72,
|
|
36
|
+
coffeeMachine: 0x4a4a52,
|
|
37
|
+
|
|
38
|
+
// Characters
|
|
39
|
+
skinLight: 0xf5c5a3,
|
|
40
|
+
skinMedium: 0xd4a574,
|
|
41
|
+
skinDark: 0x8b6340,
|
|
42
|
+
hairBlack: 0x2a2018,
|
|
43
|
+
hairBrown: 0x6a4a2a,
|
|
44
|
+
hairBlonde: 0xd4a840,
|
|
45
|
+
hairRed: 0xb04020,
|
|
46
|
+
shirtBlue: 0x4a78b0,
|
|
47
|
+
shirtGreen: 0x4a8a4a,
|
|
48
|
+
shirtRed: 0xa84848,
|
|
49
|
+
shirtWhite: 0xe0d8cc,
|
|
50
|
+
shirtPurple: 0x7a58a0,
|
|
51
|
+
pantsDark: 0x3a3a4a,
|
|
52
|
+
shoeDark: 0x2a2018,
|
|
53
|
+
|
|
54
|
+
// Character shading (auto-derived tones)
|
|
55
|
+
skinLightShadow: 0xd4a883,
|
|
56
|
+
skinMediumShadow: 0xb48854,
|
|
57
|
+
skinDarkShadow: 0x6b4320,
|
|
58
|
+
|
|
59
|
+
hairBlackLight: 0x3a3028,
|
|
60
|
+
hairBlackDark: 0x1a1008,
|
|
61
|
+
hairBrownLight: 0x8a6a4a,
|
|
62
|
+
hairBrownDark: 0x4a2a0a,
|
|
63
|
+
hairBlondeLight: 0xe4b850,
|
|
64
|
+
hairBlondeDark: 0xb48830,
|
|
65
|
+
hairRedLight: 0xc05030,
|
|
66
|
+
hairRedDark: 0x903010,
|
|
67
|
+
|
|
68
|
+
shirtBlueLight: 0x5a8ac0,
|
|
69
|
+
shirtBlueDark: 0x3a6898,
|
|
70
|
+
shirtGreenLight: 0x5a9a5a,
|
|
71
|
+
shirtGreenDark: 0x3a7a3a,
|
|
72
|
+
shirtRedLight: 0xb85858,
|
|
73
|
+
shirtRedDark: 0x983838,
|
|
74
|
+
shirtWhiteLight: 0xf0e8dc,
|
|
75
|
+
shirtWhiteDark: 0xd0c8bc,
|
|
76
|
+
shirtPurpleLight: 0x8a68b0,
|
|
77
|
+
shirtPurpleDark: 0x6a4890,
|
|
78
|
+
|
|
79
|
+
pantsBase: 0x3a3a4a,
|
|
80
|
+
pantsShade: 0x2a2a3a, // darker shade for leg edges/inner shadow
|
|
81
|
+
|
|
82
|
+
shoeBase: 0x2a2018,
|
|
83
|
+
shoeLight: 0x3a3028,
|
|
84
|
+
|
|
85
|
+
// Accessories
|
|
86
|
+
mugBody: 0xe0e0e0,
|
|
87
|
+
mugRim: 0xcccccc,
|
|
88
|
+
mugHandle: 0xcccccc,
|
|
89
|
+
postItYellow: 0xffee55,
|
|
90
|
+
postItPink: 0xff8866,
|
|
91
|
+
bookRed: 0xcc4444,
|
|
92
|
+
bookBlue: 0x4466aa,
|
|
93
|
+
bookGreen: 0x44aa44,
|
|
94
|
+
photoFrame: 0x3a3028,
|
|
95
|
+
waterBottle: 0x88bbdd,
|
|
96
|
+
waterCap: 0x4488aa,
|
|
97
|
+
|
|
98
|
+
// Name card
|
|
99
|
+
nameCardBg: 0x14141c,
|
|
100
|
+
nameCardText: 0xffffff,
|
|
101
|
+
|
|
102
|
+
// Belt
|
|
103
|
+
beltBuckle: 0x8a8a6a,
|
|
104
|
+
|
|
105
|
+
// Collar
|
|
106
|
+
collarWhite: 0xf0f0f0,
|
|
107
|
+
|
|
108
|
+
// Status effects (high contrast)
|
|
109
|
+
statusIdle: 0xaaaacc,
|
|
110
|
+
statusWorking: 0x60b0ff,
|
|
111
|
+
statusDone: 0x60f080,
|
|
112
|
+
statusCheckpoint: 0xffbb22,
|
|
113
|
+
bubbleBg: 0xffffff,
|
|
114
|
+
bubbleBorder: 0x3a3a4a,
|
|
115
|
+
particleGreen: 0x60f080,
|
|
116
|
+
|
|
117
|
+
// Envelope
|
|
118
|
+
envelopeBody: 0xf5e6c8,
|
|
119
|
+
envelopeFold: 0xe0d0b0,
|
|
120
|
+
envelopeSeal: 0xcc3333,
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
// === Layout Constants ===
|
|
124
|
+
export const TILE = 32;
|
|
125
|
+
export const CELL_W = 4 * TILE; // 128px wide per cell (spacious)
|
|
126
|
+
export const CELL_H = 4 * TILE; // 128px tall per cell
|
|
127
|
+
export const SCENE_SCALE = 3; // Integer scaling — crisp pixel art
|
|
128
|
+
|
|
129
|
+
export type CharacterColors = {
|
|
130
|
+
hair: number; hairLight: number; hairDark: number;
|
|
131
|
+
skin: number; skinShadow: number;
|
|
132
|
+
shirt: number; shirtLight: number; shirtDark: number;
|
|
133
|
+
pants: number; pantsDark: number;
|
|
134
|
+
shoe: number; shoeLight: number;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Character variants (assigned round-robin to agents)
|
|
138
|
+
export const CHARACTER_VARIANTS: CharacterColors[] = [
|
|
139
|
+
{
|
|
140
|
+
hair: COLORS.hairBlack, hairLight: COLORS.hairBlackLight, hairDark: COLORS.hairBlackDark,
|
|
141
|
+
skin: COLORS.skinLight, skinShadow: COLORS.skinLightShadow,
|
|
142
|
+
shirt: COLORS.shirtBlue, shirtLight: COLORS.shirtBlueLight, shirtDark: COLORS.shirtBlueDark,
|
|
143
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
144
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
hair: COLORS.hairBrown, hairLight: COLORS.hairBrownLight, hairDark: COLORS.hairBrownDark,
|
|
148
|
+
skin: COLORS.skinMedium, skinShadow: COLORS.skinMediumShadow,
|
|
149
|
+
shirt: COLORS.shirtGreen, shirtLight: COLORS.shirtGreenLight, shirtDark: COLORS.shirtGreenDark,
|
|
150
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
151
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
hair: COLORS.hairBlonde, hairLight: COLORS.hairBlondeLight, hairDark: COLORS.hairBlondeDark,
|
|
155
|
+
skin: COLORS.skinLight, skinShadow: COLORS.skinLightShadow,
|
|
156
|
+
shirt: COLORS.shirtRed, shirtLight: COLORS.shirtRedLight, shirtDark: COLORS.shirtRedDark,
|
|
157
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
158
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
hair: COLORS.hairRed, hairLight: COLORS.hairRedLight, hairDark: COLORS.hairRedDark,
|
|
162
|
+
skin: COLORS.skinDark, skinShadow: COLORS.skinDarkShadow,
|
|
163
|
+
shirt: COLORS.shirtWhite, shirtLight: COLORS.shirtWhiteLight, shirtDark: COLORS.shirtWhiteDark,
|
|
164
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
165
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
hair: COLORS.hairBlack, hairLight: COLORS.hairBlackLight, hairDark: COLORS.hairBlackDark,
|
|
169
|
+
skin: COLORS.skinMedium, skinShadow: COLORS.skinMediumShadow,
|
|
170
|
+
shirt: COLORS.shirtPurple, shirtLight: COLORS.shirtPurpleLight, shirtDark: COLORS.shirtPurpleDark,
|
|
171
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
172
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
hair: COLORS.hairBrown, hairLight: COLORS.hairBrownLight, hairDark: COLORS.hairBrownDark,
|
|
176
|
+
skin: COLORS.skinLight, skinShadow: COLORS.skinLightShadow,
|
|
177
|
+
shirt: COLORS.shirtGreen, shirtLight: COLORS.shirtGreenLight, shirtDark: COLORS.shirtGreenDark,
|
|
178
|
+
pants: COLORS.pantsBase, pantsDark: COLORS.pantsShade,
|
|
179
|
+
shoe: COLORS.shoeBase, shoeLight: COLORS.shoeLight,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Texture, CanvasSource } from "pixi.js";
|
|
2
|
+
import { COLORS, CharacterColors } from "./palette";
|
|
3
|
+
|
|
4
|
+
function hexToRgb(hex: number): [number, number, number] {
|
|
5
|
+
return [(hex >> 16) & 0xff, (hex >> 8) & 0xff, hex & 0xff];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createCanvas(w: number, h: number): [HTMLCanvasElement, CanvasRenderingContext2D] {
|
|
9
|
+
const canvas = document.createElement("canvas");
|
|
10
|
+
canvas.width = w;
|
|
11
|
+
canvas.height = h;
|
|
12
|
+
const ctx = canvas.getContext("2d")!;
|
|
13
|
+
ctx.imageSmoothingEnabled = false;
|
|
14
|
+
return [canvas, ctx];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 1 pixel per logical pixel — SCENE_SCALE=2 handles display scaling
|
|
18
|
+
function px(ctx: CanvasRenderingContext2D, x: number, y: number, color: number) {
|
|
19
|
+
const [r, g, b] = hexToRgb(color);
|
|
20
|
+
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
21
|
+
ctx.fillRect(x, y, 1, 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hspan(ctx: CanvasRenderingContext2D, x1: number, x2: number, y: number, color: number) {
|
|
25
|
+
for (let x = x1; x <= x2; x++) px(ctx, x, y, color);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type MouthVariant = "neutral" | "focused" | "smile";
|
|
29
|
+
|
|
30
|
+
function drawHead(ctx: CanvasRenderingContext2D, c: CharacterColors, mouth: MouthVariant) {
|
|
31
|
+
// --- HAIR (rows 2-7) ---
|
|
32
|
+
hspan(ctx, 16, 30, 2, c.hair);
|
|
33
|
+
hspan(ctx, 15, 31, 3, c.hair);
|
|
34
|
+
hspan(ctx, 14, 32, 4, c.hair);
|
|
35
|
+
hspan(ctx, 14, 32, 5, c.hair);
|
|
36
|
+
// Hair highlights
|
|
37
|
+
px(ctx, 17, 3, c.hairLight); px(ctx, 20, 3, c.hairLight); px(ctx, 24, 3, c.hairLight);
|
|
38
|
+
px(ctx, 25, 4, c.hairLight); px(ctx, 28, 3, c.hairLight);
|
|
39
|
+
px(ctx, 16, 4, c.hairLight); px(ctx, 22, 4, c.hairLight); px(ctx, 30, 4, c.hairLight);
|
|
40
|
+
// Hair dark edges
|
|
41
|
+
px(ctx, 14, 5, c.hairDark); px(ctx, 32, 5, c.hairDark);
|
|
42
|
+
px(ctx, 15, 4, c.hairDark); px(ctx, 31, 4, c.hairDark);
|
|
43
|
+
// Sideburns
|
|
44
|
+
px(ctx, 14, 6, c.hair); px(ctx, 14, 7, c.hair);
|
|
45
|
+
px(ctx, 32, 6, c.hair); px(ctx, 32, 7, c.hair);
|
|
46
|
+
|
|
47
|
+
// --- FACE (rows 6-14) with oval contour ---
|
|
48
|
+
hspan(ctx, 15, 31, 6, c.skin);
|
|
49
|
+
hspan(ctx, 15, 31, 7, c.skin);
|
|
50
|
+
hspan(ctx, 15, 31, 8, c.skin);
|
|
51
|
+
hspan(ctx, 15, 31, 9, c.skin);
|
|
52
|
+
hspan(ctx, 15, 31, 10, c.skin);
|
|
53
|
+
hspan(ctx, 16, 30, 11, c.skin);
|
|
54
|
+
hspan(ctx, 17, 29, 12, c.skin);
|
|
55
|
+
hspan(ctx, 18, 28, 13, c.skin);
|
|
56
|
+
hspan(ctx, 19, 27, 14, c.skin);
|
|
57
|
+
// Oval contour shadows — left edge
|
|
58
|
+
px(ctx, 15, 8, c.skinShadow); px(ctx, 15, 9, c.skinShadow); px(ctx, 15, 10, c.skinShadow);
|
|
59
|
+
px(ctx, 16, 11, c.skinShadow); px(ctx, 17, 12, c.skinShadow);
|
|
60
|
+
px(ctx, 18, 13, c.skinShadow); px(ctx, 19, 14, c.skinShadow);
|
|
61
|
+
// Oval contour shadows — right edge
|
|
62
|
+
px(ctx, 31, 8, c.skinShadow); px(ctx, 31, 9, c.skinShadow); px(ctx, 31, 10, c.skinShadow);
|
|
63
|
+
px(ctx, 30, 11, c.skinShadow); px(ctx, 29, 12, c.skinShadow);
|
|
64
|
+
px(ctx, 28, 13, c.skinShadow); px(ctx, 27, 14, c.skinShadow);
|
|
65
|
+
// Jaw shadow
|
|
66
|
+
hspan(ctx, 20, 26, 14, c.skinShadow);
|
|
67
|
+
|
|
68
|
+
// Eyebrows (4px wide, 1 row above eyes)
|
|
69
|
+
hspan(ctx, 17, 20, 7, c.hairDark);
|
|
70
|
+
hspan(ctx, 26, 29, 7, c.hairDark);
|
|
71
|
+
|
|
72
|
+
// Eyes (5px wide: white-white-pupil-white-white) — focused shifts down 1px
|
|
73
|
+
const eyeY = mouth === "focused" ? 10 : 9;
|
|
74
|
+
px(ctx, 17, eyeY, 0xf0ede8); px(ctx, 18, eyeY, 0xf0ede8);
|
|
75
|
+
px(ctx, 19, eyeY, 0x2a2018); px(ctx, 20, eyeY, 0x2a2018);
|
|
76
|
+
px(ctx, 21, eyeY, 0xf0ede8);
|
|
77
|
+
px(ctx, 25, eyeY, 0xf0ede8);
|
|
78
|
+
px(ctx, 26, eyeY, 0x2a2018); px(ctx, 27, eyeY, 0x2a2018);
|
|
79
|
+
px(ctx, 28, eyeY, 0xf0ede8); px(ctx, 29, eyeY, 0xf0ede8);
|
|
80
|
+
|
|
81
|
+
// Nose (L-shape, more defined)
|
|
82
|
+
px(ctx, 23, 10, c.skinShadow);
|
|
83
|
+
px(ctx, 23, 11, c.skinShadow);
|
|
84
|
+
px(ctx, 23, 12, c.skinShadow);
|
|
85
|
+
px(ctx, 24, 12, c.skinShadow);
|
|
86
|
+
|
|
87
|
+
// Mouth
|
|
88
|
+
if (mouth === "smile") {
|
|
89
|
+
// Smile: corners up + bottom curve
|
|
90
|
+
px(ctx, 20, 13, 0x2a2018); px(ctx, 26, 13, 0x2a2018);
|
|
91
|
+
hspan(ctx, 21, 25, 14, 0x2a2018);
|
|
92
|
+
// Lower lip highlight
|
|
93
|
+
hspan(ctx, 22, 24, 15, c.skinShadow);
|
|
94
|
+
} else {
|
|
95
|
+
// neutral / focused: two-row mouth with lip
|
|
96
|
+
hspan(ctx, 21, 25, 13, 0x2a2018);
|
|
97
|
+
hspan(ctx, 22, 24, 14, c.skinShadow);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ears (2px tall, with shadow)
|
|
101
|
+
px(ctx, 14, 8, c.skin); px(ctx, 14, 9, c.skin); px(ctx, 14, 10, c.skinShadow);
|
|
102
|
+
px(ctx, 32, 8, c.skin); px(ctx, 32, 9, c.skin); px(ctx, 32, 10, c.skinShadow);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function drawBody(ctx: CanvasRenderingContext2D, c: CharacterColors) {
|
|
106
|
+
// --- NECK (rows 15-16) ---
|
|
107
|
+
hspan(ctx, 20, 26, 15, c.skin);
|
|
108
|
+
hspan(ctx, 21, 25, 16, c.skin);
|
|
109
|
+
px(ctx, 20, 15, c.skinShadow); px(ctx, 26, 15, c.skinShadow);
|
|
110
|
+
|
|
111
|
+
// --- COLLAR (row 17) ---
|
|
112
|
+
hspan(ctx, 17, 29, 17, COLORS.collarWhite);
|
|
113
|
+
px(ctx, 22, 17, 0xe0e0e0); px(ctx, 23, 17, 0xe0e0e0); px(ctx, 24, 17, 0xe0e0e0);
|
|
114
|
+
|
|
115
|
+
// --- SHIRT (rows 18-28) ---
|
|
116
|
+
for (let y = 18; y <= 28; y++) {
|
|
117
|
+
for (let i = 13; i <= 33; i++) {
|
|
118
|
+
if (i <= 15) px(ctx, i, y, c.shirtDark);
|
|
119
|
+
else if (i >= 31) px(ctx, i, y, c.shirtDark);
|
|
120
|
+
else if (i >= 22 && i <= 24) px(ctx, i, y, c.shirtLight);
|
|
121
|
+
else px(ctx, i, y, c.shirt);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- BELT (row 29) ---
|
|
126
|
+
hspan(ctx, 13, 33, 29, c.pantsDark);
|
|
127
|
+
px(ctx, 22, 29, COLORS.beltBuckle); px(ctx, 23, 29, COLORS.beltBuckle); px(ctx, 24, 29, COLORS.beltBuckle);
|
|
128
|
+
|
|
129
|
+
// --- PANTS (rows 30-39) ---
|
|
130
|
+
for (let y = 30; y <= 39; y++) {
|
|
131
|
+
for (let i = 14; i <= 21; i++) px(ctx, i, y, i <= 15 ? c.pantsDark : c.pants);
|
|
132
|
+
for (let i = 25; i <= 32; i++) px(ctx, i, y, i >= 31 ? c.pantsDark : c.pants);
|
|
133
|
+
px(ctx, 21, y, c.pantsDark); px(ctx, 25, y, c.pantsDark);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- SHOES (rows 40-43) ---
|
|
137
|
+
for (let i = 13; i <= 22; i++) { px(ctx, i, 40, c.shoe); px(ctx, i, 41, c.shoe); }
|
|
138
|
+
for (let i = 13; i <= 22; i++) px(ctx, i, 42, i <= 14 ? c.shoeLight : c.shoe);
|
|
139
|
+
hspan(ctx, 13, 22, 43, c.shoeLight);
|
|
140
|
+
for (let i = 24; i <= 33; i++) { px(ctx, i, 40, c.shoe); px(ctx, i, 41, c.shoe); }
|
|
141
|
+
for (let i = 24; i <= 33; i++) px(ctx, i, 42, i >= 32 ? c.shoeLight : c.shoe);
|
|
142
|
+
hspan(ctx, 24, 33, 43, c.shoeLight);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function drawCharacterIdle(ctx: CanvasRenderingContext2D, c: CharacterColors) {
|
|
146
|
+
drawHead(ctx, c, "neutral");
|
|
147
|
+
drawBody(ctx, c);
|
|
148
|
+
|
|
149
|
+
// Left arm at side — sleeve (3px wide)
|
|
150
|
+
for (let y = 18; y <= 22; y++) { px(ctx, 10, y, c.shirtDark); px(ctx, 11, y, c.shirt); px(ctx, 12, y, c.shirt); }
|
|
151
|
+
// Left forearm (3px wide with shadow)
|
|
152
|
+
for (let y = 23; y <= 27; y++) { px(ctx, 9, y, c.skinShadow); px(ctx, 10, y, c.skin); px(ctx, 11, y, c.skin); }
|
|
153
|
+
// Left hand (4px wide)
|
|
154
|
+
px(ctx, 8, 28, c.skin); px(ctx, 9, 28, c.skin); px(ctx, 10, 28, c.skin); px(ctx, 11, 28, c.skin);
|
|
155
|
+
px(ctx, 8, 29, c.skinShadow); px(ctx, 9, 29, c.skinShadow); px(ctx, 10, 29, c.skin);
|
|
156
|
+
|
|
157
|
+
// Right arm at side — sleeve (3px wide)
|
|
158
|
+
for (let y = 18; y <= 22; y++) { px(ctx, 34, y, c.shirt); px(ctx, 35, y, c.shirt); px(ctx, 36, y, c.shirtDark); }
|
|
159
|
+
// Right forearm (3px wide with shadow)
|
|
160
|
+
for (let y = 23; y <= 27; y++) { px(ctx, 35, y, c.skin); px(ctx, 36, y, c.skin); px(ctx, 37, y, c.skinShadow); }
|
|
161
|
+
// Right hand (4px wide)
|
|
162
|
+
px(ctx, 35, 28, c.skin); px(ctx, 36, 28, c.skin); px(ctx, 37, 28, c.skin); px(ctx, 38, 28, c.skin);
|
|
163
|
+
px(ctx, 36, 29, c.skin); px(ctx, 37, 29, c.skinShadow); px(ctx, 38, 29, c.skinShadow);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function drawCharacterWorking(ctx: CanvasRenderingContext2D, c: CharacterColors, frame: 0 | 1) {
|
|
167
|
+
drawHead(ctx, c, "focused");
|
|
168
|
+
drawBody(ctx, c);
|
|
169
|
+
|
|
170
|
+
// Arms forward (typing) — 3px wide sleeves + forearms
|
|
171
|
+
if (frame === 0) {
|
|
172
|
+
// Left arm: sleeve reaching forward
|
|
173
|
+
for (let y = 18; y <= 20; y++) { px(ctx, 10, y, c.shirtDark); px(ctx, 11, y, c.shirt); px(ctx, 12, y, c.shirt); }
|
|
174
|
+
// Left forearm
|
|
175
|
+
for (let y = 21; y <= 24; y++) { px(ctx, 9, y, c.skinShadow); px(ctx, 10, y, c.skin); px(ctx, 11, y, c.skin); }
|
|
176
|
+
// Left hand on keyboard
|
|
177
|
+
px(ctx, 10, 25, c.skin); px(ctx, 11, 25, c.skin); px(ctx, 12, 25, c.skin); px(ctx, 13, 25, c.skin);
|
|
178
|
+
// Right arm
|
|
179
|
+
for (let y = 18; y <= 20; y++) { px(ctx, 34, y, c.shirt); px(ctx, 35, y, c.shirt); px(ctx, 36, y, c.shirtDark); }
|
|
180
|
+
for (let y = 21; y <= 24; y++) { px(ctx, 35, y, c.skin); px(ctx, 36, y, c.skin); px(ctx, 37, y, c.skinShadow); }
|
|
181
|
+
px(ctx, 33, 25, c.skin); px(ctx, 34, 25, c.skin); px(ctx, 35, 25, c.skin); px(ctx, 36, 25, c.skin);
|
|
182
|
+
} else {
|
|
183
|
+
// Left arm: slightly raised (keystroke)
|
|
184
|
+
for (let y = 18; y <= 20; y++) { px(ctx, 10, y, c.shirtDark); px(ctx, 11, y, c.shirt); px(ctx, 12, y, c.shirt); }
|
|
185
|
+
for (let y = 21; y <= 23; y++) { px(ctx, 9, y, c.skinShadow); px(ctx, 10, y, c.skin); px(ctx, 11, y, c.skin); }
|
|
186
|
+
px(ctx, 10, 24, c.skin); px(ctx, 11, 24, c.skin); px(ctx, 12, 24, c.skin); px(ctx, 13, 24, c.skin);
|
|
187
|
+
// Right arm
|
|
188
|
+
for (let y = 18; y <= 20; y++) { px(ctx, 34, y, c.shirt); px(ctx, 35, y, c.shirt); px(ctx, 36, y, c.shirtDark); }
|
|
189
|
+
for (let y = 21; y <= 23; y++) { px(ctx, 35, y, c.skin); px(ctx, 36, y, c.skin); px(ctx, 37, y, c.skinShadow); }
|
|
190
|
+
px(ctx, 33, 24, c.skin); px(ctx, 34, 24, c.skin); px(ctx, 35, 24, c.skin); px(ctx, 36, 24, c.skin);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function drawCharacterDone(ctx: CanvasRenderingContext2D, c: CharacterColors) {
|
|
195
|
+
drawHead(ctx, c, "smile");
|
|
196
|
+
drawBody(ctx, c);
|
|
197
|
+
|
|
198
|
+
// Arms raised (celebration) — 3px wide, diagonal up
|
|
199
|
+
// Left sleeve
|
|
200
|
+
px(ctx, 10, 18, c.shirtDark); px(ctx, 11, 18, c.shirt); px(ctx, 12, 18, c.shirt);
|
|
201
|
+
px(ctx, 10, 17, c.shirt); px(ctx, 11, 17, c.shirt);
|
|
202
|
+
// Left arm going up-left (3px wide diagonal)
|
|
203
|
+
px(ctx, 9, 16, c.skin); px(ctx, 10, 16, c.skin); px(ctx, 10, 15, c.skinShadow);
|
|
204
|
+
px(ctx, 8, 14, c.skin); px(ctx, 9, 14, c.skin); px(ctx, 9, 13, c.skinShadow);
|
|
205
|
+
px(ctx, 7, 12, c.skin); px(ctx, 8, 12, c.skin);
|
|
206
|
+
px(ctx, 6, 10, c.skin); px(ctx, 7, 10, c.skin); px(ctx, 7, 11, c.skin);
|
|
207
|
+
px(ctx, 5, 8, c.skin); px(ctx, 6, 8, c.skin); px(ctx, 6, 9, c.skin);
|
|
208
|
+
|
|
209
|
+
// Right sleeve
|
|
210
|
+
px(ctx, 34, 18, c.shirt); px(ctx, 35, 18, c.shirt); px(ctx, 36, 18, c.shirtDark);
|
|
211
|
+
px(ctx, 35, 17, c.shirt); px(ctx, 36, 17, c.shirt);
|
|
212
|
+
// Right arm going up-right
|
|
213
|
+
px(ctx, 36, 16, c.skin); px(ctx, 37, 16, c.skin); px(ctx, 36, 15, c.skinShadow);
|
|
214
|
+
px(ctx, 37, 14, c.skin); px(ctx, 38, 14, c.skin); px(ctx, 37, 13, c.skinShadow);
|
|
215
|
+
px(ctx, 38, 12, c.skin); px(ctx, 39, 12, c.skin);
|
|
216
|
+
px(ctx, 39, 10, c.skin); px(ctx, 40, 10, c.skin); px(ctx, 39, 11, c.skin);
|
|
217
|
+
px(ctx, 40, 8, c.skin); px(ctx, 41, 8, c.skin); px(ctx, 40, 9, c.skin);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface CharacterTextures {
|
|
221
|
+
idle: Texture;
|
|
222
|
+
working: [Texture, Texture];
|
|
223
|
+
done: Texture;
|
|
224
|
+
checkpoint: Texture;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function generateCharacterTextures(colors: CharacterColors): CharacterTextures {
|
|
228
|
+
const size = 48;
|
|
229
|
+
|
|
230
|
+
function makeFrame(drawFn: (ctx: CanvasRenderingContext2D) => void): Texture {
|
|
231
|
+
const [canvas, ctx] = createCanvas(size, size);
|
|
232
|
+
drawFn(ctx);
|
|
233
|
+
return new Texture({ source: new CanvasSource({ resource: canvas, scaleMode: "nearest" }) });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
idle: makeFrame((ctx) => drawCharacterIdle(ctx, colors)),
|
|
238
|
+
working: [
|
|
239
|
+
makeFrame((ctx) => drawCharacterWorking(ctx, colors, 0)),
|
|
240
|
+
makeFrame((ctx) => drawCharacterWorking(ctx, colors, 1)),
|
|
241
|
+
],
|
|
242
|
+
done: makeFrame((ctx) => drawCharacterDone(ctx, colors)),
|
|
243
|
+
checkpoint: makeFrame((ctx) => drawCharacterIdle(ctx, colors)),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const textureCache = new Map<number, CharacterTextures>();
|
|
248
|
+
|
|
249
|
+
export function getCharacterTextures(variantIndex: number, colors: CharacterColors): CharacterTextures {
|
|
250
|
+
if (!textureCache.has(variantIndex)) {
|
|
251
|
+
textureCache.set(variantIndex, generateCharacterTextures(colors));
|
|
252
|
+
}
|
|
253
|
+
return textureCache.get(variantIndex)!;
|
|
254
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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 path from "node:path";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
import type { DepartmentInfo, DepartmentState, WsMessage } from "../types/state";
|
|
9
|
+
|
|
10
|
+
function resolveDepartmentsDir(): string {
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.resolve(process.cwd(), "../departments"), // started from dashboard/
|
|
13
|
+
path.resolve(process.cwd(), "departments"), // started from project root
|
|
14
|
+
];
|
|
15
|
+
for (const c of candidates) {
|
|
16
|
+
if (fs.existsSync(c)) return c;
|
|
17
|
+
}
|
|
18
|
+
return path.resolve(process.cwd(), "../departments"); // default (will be created on demand)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function discoverDepartments(departmentsDir: string): DepartmentInfo[] {
|
|
22
|
+
if (!fs.existsSync(departmentsDir)) return [];
|
|
23
|
+
|
|
24
|
+
const entries = fs.readdirSync(departmentsDir, { withFileTypes: true });
|
|
25
|
+
const departments: DepartmentInfo[] = [];
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory()) continue;
|
|
29
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
30
|
+
|
|
31
|
+
const yamlPath = path.join(departmentsDir, entry.name, "department.yaml");
|
|
32
|
+
if (fs.existsSync(yamlPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(yamlPath, "utf-8");
|
|
35
|
+
const parsed = parseYaml(raw);
|
|
36
|
+
const s = parsed?.department;
|
|
37
|
+
if (s) {
|
|
38
|
+
departments.push({
|
|
39
|
+
code: typeof s.code === "string" ? s.code : entry.name,
|
|
40
|
+
name: typeof s.name === "string" ? s.name : entry.name,
|
|
41
|
+
description: typeof s.description === "string" ? s.description : "",
|
|
42
|
+
icon: typeof s.icon === "string" ? s.icon : "\u{1F4CB}",
|
|
43
|
+
agents: Array.isArray(s.agents) ? (s.agents as unknown[]).filter((a): a is string => typeof a === "string") : [],
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Fall through to default
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// No department.yaml or invalid YAML — use directory name as fallback
|
|
53
|
+
departments.push({
|
|
54
|
+
code: entry.name,
|
|
55
|
+
name: entry.name,
|
|
56
|
+
description: "",
|
|
57
|
+
icon: "\u{1F4CB}",
|
|
58
|
+
agents: [],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return departments;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidState(data: unknown): data is DepartmentState {
|
|
66
|
+
if (!data || typeof data !== "object") return false;
|
|
67
|
+
const d = data as Record<string, unknown>;
|
|
68
|
+
return (
|
|
69
|
+
typeof d.status === "string" &&
|
|
70
|
+
d.step != null && typeof d.step === "object" &&
|
|
71
|
+
Array.isArray(d.agents)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readActiveStates(departmentsDir: string): Record<string, DepartmentState> {
|
|
76
|
+
const states: Record<string, DepartmentState> = {};
|
|
77
|
+
if (!fs.existsSync(departmentsDir)) return states;
|
|
78
|
+
|
|
79
|
+
const entries = fs.readdirSync(departmentsDir, { withFileTypes: true });
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (!entry.isDirectory()) continue;
|
|
82
|
+
const statePath = path.join(departmentsDir, entry.name, "state.json");
|
|
83
|
+
if (!fs.existsSync(statePath)) continue;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(statePath, "utf-8");
|
|
87
|
+
const parsed = JSON.parse(raw);
|
|
88
|
+
if (isValidState(parsed)) {
|
|
89
|
+
states[entry.name] = parsed;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Skip invalid JSON
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return states;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildSnapshot(departmentsDir: string): WsMessage {
|
|
100
|
+
return {
|
|
101
|
+
type: "SNAPSHOT",
|
|
102
|
+
departments: discoverDepartments(departmentsDir),
|
|
103
|
+
activeStates: readActiveStates(departmentsDir),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function broadcast(wss: WebSocketServer, msg: WsMessage) {
|
|
108
|
+
const data = JSON.stringify(msg);
|
|
109
|
+
for (const client of wss.clients) {
|
|
110
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
111
|
+
client.send(data);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function departmentWatcherPlugin(): Plugin {
|
|
117
|
+
return {
|
|
118
|
+
name: "department-watcher",
|
|
119
|
+
configureServer(server: ViteDevServer) {
|
|
120
|
+
const departmentsDir = resolveDepartmentsDir();
|
|
121
|
+
server.config.logger.info(`[department-watcher] departments dir: ${departmentsDir}`);
|
|
122
|
+
|
|
123
|
+
// Create WebSocket server with noServer to avoid intercepting Vite's HMR
|
|
124
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
125
|
+
(server.httpServer as Server).on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
126
|
+
if (req.url === "/__departments_ws") {
|
|
127
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
128
|
+
wss.emit("connection", ws, req);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Let Vite handle all other upgrade requests (HMR)
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Send snapshot on new connection
|
|
135
|
+
wss.on("connection", (ws) => {
|
|
136
|
+
ws.send(JSON.stringify(buildSnapshot(departmentsDir)));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Ensure departments directory exists
|
|
140
|
+
if (!fs.existsSync(departmentsDir)) {
|
|
141
|
+
fs.mkdirSync(departmentsDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Watch state.json files using Vite's built-in chokidar watcher
|
|
145
|
+
const stateGlob = path.join(departmentsDir, "*/state.json").replace(/\\/g, "/");
|
|
146
|
+
server.watcher.add(stateGlob);
|
|
147
|
+
|
|
148
|
+
// Debounce timers per department to avoid reading partial writes
|
|
149
|
+
const changeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
150
|
+
|
|
151
|
+
// Also watch for new department.yaml files
|
|
152
|
+
const yamlGlob = path.join(departmentsDir, "*/department.yaml").replace(/\\/g, "/");
|
|
153
|
+
server.watcher.add(yamlGlob);
|
|
154
|
+
|
|
155
|
+
server.watcher.on("add", (filePath: string) => {
|
|
156
|
+
if (filePath.endsWith("state.json")) {
|
|
157
|
+
const departmentName = extractDepartmentName(filePath, departmentsDir);
|
|
158
|
+
if (!departmentName) return;
|
|
159
|
+
clearTimeout(changeTimers.get(departmentName));
|
|
160
|
+
changeTimers.set(departmentName, setTimeout(() => {
|
|
161
|
+
try {
|
|
162
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
163
|
+
const state: DepartmentState = JSON.parse(raw);
|
|
164
|
+
broadcast(wss, { type: "DEPARTMENT_ACTIVE", department: departmentName, state });
|
|
165
|
+
} catch { /* skip */ }
|
|
166
|
+
}, 50));
|
|
167
|
+
} else if (filePath.endsWith("department.yaml")) {
|
|
168
|
+
broadcast(wss, buildSnapshot(departmentsDir));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
server.watcher.on("change", (filePath: string) => {
|
|
173
|
+
if (filePath.endsWith("state.json")) {
|
|
174
|
+
const departmentName = extractDepartmentName(filePath, departmentsDir);
|
|
175
|
+
if (!departmentName) return;
|
|
176
|
+
clearTimeout(changeTimers.get(departmentName));
|
|
177
|
+
changeTimers.set(departmentName, setTimeout(() => {
|
|
178
|
+
try {
|
|
179
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
180
|
+
const state: DepartmentState = JSON.parse(raw);
|
|
181
|
+
broadcast(wss, { type: "DEPARTMENT_UPDATE", department: departmentName, state });
|
|
182
|
+
} catch { /* skip */ }
|
|
183
|
+
}, 50));
|
|
184
|
+
} else if (filePath.endsWith("department.yaml")) {
|
|
185
|
+
broadcast(wss, buildSnapshot(departmentsDir));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
server.watcher.on("unlink", (filePath: string) => {
|
|
190
|
+
if (filePath.endsWith("state.json")) {
|
|
191
|
+
const departmentName = extractDepartmentName(filePath, departmentsDir);
|
|
192
|
+
if (!departmentName) return;
|
|
193
|
+
clearTimeout(changeTimers.get(departmentName));
|
|
194
|
+
changeTimers.delete(departmentName);
|
|
195
|
+
broadcast(wss, { type: "DEPARTMENT_INACTIVE", department: departmentName });
|
|
196
|
+
} else if (filePath.endsWith("department.yaml")) {
|
|
197
|
+
broadcast(wss, buildSnapshot(departmentsDir));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function extractDepartmentName(filePath: string, departmentsDir: string): string | null {
|
|
205
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
206
|
+
const normalizedBase = departmentsDir.replace(/\\/g, "/");
|
|
207
|
+
const relative = normalized.replace(normalizedBase + "/", "");
|
|
208
|
+
const parts = relative.split("/");
|
|
209
|
+
return parts.length >= 2 ? parts[0] : null;
|
|
210
|
+
}
|