@cliangdev/flux-plugin 0.2.0-dev.e34d43b → 0.2.0-dev.f718bcf
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/agents/coder.md +150 -25
- package/commands/breakdown.md +44 -7
- package/commands/implement.md +165 -15
- package/commands/prd.md +176 -1
- package/manifest.json +2 -1
- package/package.json +4 -2
- package/skills/prd-writer/SKILL.md +184 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +52 -18
- package/src/server/adapters/linear/adapter.ts +19 -14
- package/src/server/adapters/local-adapter.ts +48 -7
- package/src/server/db/__tests__/queries.test.ts +2 -1
- package/src/server/db/schema.ts +9 -0
- package/src/server/tools/__tests__/crud.test.ts +111 -1
- package/src/server/tools/__tests__/mcp-interface.test.ts +2 -1
- package/src/server/tools/__tests__/query.test.ts +73 -2
- package/src/server/tools/__tests__/z-configure-linear.test.ts +1 -1
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +1 -1
- package/src/server/tools/create-epic.ts +11 -2
- package/src/server/tools/create-prd.ts +11 -2
- package/src/server/tools/create-task.ts +11 -2
- package/src/server/tools/dependencies.ts +2 -2
- package/src/server/tools/get-entity.ts +12 -10
- package/src/server/tools/render-status.ts +38 -20
- package/src/status-line/__tests__/status-line.test.ts +1 -1
- package/src/utils/status-renderer.ts +32 -6
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Design Tokens Reference
|
|
2
|
+
|
|
3
|
+
Standardized design values for consistent, scalable interfaces.
|
|
4
|
+
|
|
5
|
+
## Spacing Scale
|
|
6
|
+
|
|
7
|
+
Use multiples of 4px for all spacing.
|
|
8
|
+
|
|
9
|
+
| Token | Value | Use Case |
|
|
10
|
+
|-------|-------|----------|
|
|
11
|
+
| `--space-0` | 0 | Reset spacing |
|
|
12
|
+
| `--space-1` | 4px | Tight: icon-text gap, inline elements |
|
|
13
|
+
| `--space-2` | 8px | Compact: related elements within a component |
|
|
14
|
+
| `--space-3` | 12px | Default: form field gaps, list item padding |
|
|
15
|
+
| `--space-4` | 16px | Standard: card padding, section gaps |
|
|
16
|
+
| `--space-5` | 20px | Comfortable: grouped content spacing |
|
|
17
|
+
| `--space-6` | 24px | Relaxed: between sections |
|
|
18
|
+
| `--space-8` | 32px | Large: major section separation |
|
|
19
|
+
| `--space-10` | 40px | Generous: page section margins |
|
|
20
|
+
| `--space-12` | 48px | Extra: hero sections, major landmarks |
|
|
21
|
+
| `--space-16` | 64px | Maximum: page-level vertical rhythm |
|
|
22
|
+
|
|
23
|
+
### CSS Variables
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
:root {
|
|
27
|
+
--space-1: 0.25rem; /* 4px */
|
|
28
|
+
--space-2: 0.5rem; /* 8px */
|
|
29
|
+
--space-3: 0.75rem; /* 12px */
|
|
30
|
+
--space-4: 1rem; /* 16px */
|
|
31
|
+
--space-5: 1.25rem; /* 20px */
|
|
32
|
+
--space-6: 1.5rem; /* 24px */
|
|
33
|
+
--space-8: 2rem; /* 32px */
|
|
34
|
+
--space-10: 2.5rem; /* 40px */
|
|
35
|
+
--space-12: 3rem; /* 48px */
|
|
36
|
+
--space-16: 4rem; /* 64px */
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Typography Scale
|
|
43
|
+
|
|
44
|
+
Based on a 1.25 ratio (major third) from 16px base.
|
|
45
|
+
|
|
46
|
+
| Token | Size | Line Height | Use Case |
|
|
47
|
+
|-------|------|-------------|----------|
|
|
48
|
+
| `--text-xs` | 12px | 1.5 | Labels, captions, fine print |
|
|
49
|
+
| `--text-sm` | 14px | 1.5 | Secondary text, table cells |
|
|
50
|
+
| `--text-base` | 16px | 1.5 | Body text, inputs |
|
|
51
|
+
| `--text-lg` | 18px | 1.5 | Lead paragraphs |
|
|
52
|
+
| `--text-xl` | 20px | 1.4 | H4, card titles |
|
|
53
|
+
| `--text-2xl` | 24px | 1.3 | H3 |
|
|
54
|
+
| `--text-3xl` | 30px | 1.3 | H2 |
|
|
55
|
+
| `--text-4xl` | 36px | 1.2 | H1, page titles |
|
|
56
|
+
| `--text-5xl` | 48px | 1.1 | Hero headings |
|
|
57
|
+
|
|
58
|
+
### Font Weights
|
|
59
|
+
|
|
60
|
+
| Token | Weight | Use Case |
|
|
61
|
+
|-------|--------|----------|
|
|
62
|
+
| `--font-normal` | 400 | Body text |
|
|
63
|
+
| `--font-medium` | 500 | Emphasis, labels |
|
|
64
|
+
| `--font-semibold` | 600 | Subheadings, buttons |
|
|
65
|
+
| `--font-bold` | 700 | Headings, strong emphasis |
|
|
66
|
+
|
|
67
|
+
### CSS Variables
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
:root {
|
|
71
|
+
/* Sizes */
|
|
72
|
+
--text-xs: 0.75rem;
|
|
73
|
+
--text-sm: 0.875rem;
|
|
74
|
+
--text-base: 1rem;
|
|
75
|
+
--text-lg: 1.125rem;
|
|
76
|
+
--text-xl: 1.25rem;
|
|
77
|
+
--text-2xl: 1.5rem;
|
|
78
|
+
--text-3xl: 1.875rem;
|
|
79
|
+
--text-4xl: 2.25rem;
|
|
80
|
+
--text-5xl: 3rem;
|
|
81
|
+
|
|
82
|
+
/* Line heights */
|
|
83
|
+
--leading-tight: 1.2;
|
|
84
|
+
--leading-snug: 1.3;
|
|
85
|
+
--leading-normal: 1.5;
|
|
86
|
+
--leading-relaxed: 1.625;
|
|
87
|
+
|
|
88
|
+
/* Weights */
|
|
89
|
+
--font-normal: 400;
|
|
90
|
+
--font-medium: 500;
|
|
91
|
+
--font-semibold: 600;
|
|
92
|
+
--font-bold: 700;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Color Tokens
|
|
99
|
+
|
|
100
|
+
### Neutral Colors (Light Mode)
|
|
101
|
+
|
|
102
|
+
| Token | Value | Use Case |
|
|
103
|
+
|-------|-------|----------|
|
|
104
|
+
| `--neutral-50` | #FAFAFA | Page background |
|
|
105
|
+
| `--neutral-100` | #F5F5F5 | Card backgrounds, subtle fills |
|
|
106
|
+
| `--neutral-200` | #E5E5E5 | Borders, dividers |
|
|
107
|
+
| `--neutral-300` | #D4D4D4 | Disabled borders |
|
|
108
|
+
| `--neutral-400` | #A3A3A3 | Placeholder text |
|
|
109
|
+
| `--neutral-500` | #737373 | Secondary text |
|
|
110
|
+
| `--neutral-600` | #525252 | Body text (secondary) |
|
|
111
|
+
| `--neutral-700` | #404040 | Body text |
|
|
112
|
+
| `--neutral-800` | #262626 | Headings |
|
|
113
|
+
| `--neutral-900` | #171717 | High contrast text |
|
|
114
|
+
|
|
115
|
+
### Semantic Colors
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
:root {
|
|
119
|
+
/* Primary */
|
|
120
|
+
--primary-50: #EFF6FF;
|
|
121
|
+
--primary-100: #DBEAFE;
|
|
122
|
+
--primary-500: #3B82F6;
|
|
123
|
+
--primary-600: #2563EB;
|
|
124
|
+
--primary-700: #1D4ED8;
|
|
125
|
+
|
|
126
|
+
/* Success */
|
|
127
|
+
--success-50: #F0FDF4;
|
|
128
|
+
--success-500: #22C55E;
|
|
129
|
+
--success-700: #15803D;
|
|
130
|
+
|
|
131
|
+
/* Warning */
|
|
132
|
+
--warning-50: #FFFBEB;
|
|
133
|
+
--warning-500: #F59E0B;
|
|
134
|
+
--warning-700: #B45309;
|
|
135
|
+
|
|
136
|
+
/* Error */
|
|
137
|
+
--error-50: #FEF2F2;
|
|
138
|
+
--error-500: #EF4444;
|
|
139
|
+
--error-700: #B91C1C;
|
|
140
|
+
|
|
141
|
+
/* Info */
|
|
142
|
+
--info-50: #EFF6FF;
|
|
143
|
+
--info-500: #3B82F6;
|
|
144
|
+
--info-700: #1D4ED8;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Dark Mode Mapping
|
|
149
|
+
|
|
150
|
+
| Light Token | Dark Equivalent |
|
|
151
|
+
|-------------|-----------------|
|
|
152
|
+
| `--neutral-50` (bg) | `--neutral-900` |
|
|
153
|
+
| `--neutral-100` (surface) | `--neutral-800` |
|
|
154
|
+
| `--neutral-200` (border) | `--neutral-700` |
|
|
155
|
+
| `--neutral-700` (text) | `--neutral-200` |
|
|
156
|
+
| `--neutral-900` (heading) | `--neutral-50` |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Border Radius
|
|
161
|
+
|
|
162
|
+
| Token | Value | Use Case |
|
|
163
|
+
|-------|-------|----------|
|
|
164
|
+
| `--radius-none` | 0 | No rounding |
|
|
165
|
+
| `--radius-sm` | 4px | Subtle rounding (inputs, buttons) |
|
|
166
|
+
| `--radius-md` | 6px | Default (cards, modals) |
|
|
167
|
+
| `--radius-lg` | 8px | Prominent elements |
|
|
168
|
+
| `--radius-xl` | 12px | Larger containers |
|
|
169
|
+
| `--radius-2xl` | 16px | Pills, avatars |
|
|
170
|
+
| `--radius-full` | 9999px | Circles, full pills |
|
|
171
|
+
|
|
172
|
+
### Consistency Rule
|
|
173
|
+
|
|
174
|
+
Pick ONE radius for each component category and stick to it:
|
|
175
|
+
- Buttons: `--radius-sm` or `--radius-md`
|
|
176
|
+
- Cards: `--radius-md` or `--radius-lg`
|
|
177
|
+
- Inputs: Same as buttons
|
|
178
|
+
- Avatars: `--radius-full`
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Shadows
|
|
183
|
+
|
|
184
|
+
| Token | Use Case |
|
|
185
|
+
|-------|----------|
|
|
186
|
+
| `--shadow-sm` | Subtle elevation (dropdowns, popovers) |
|
|
187
|
+
| `--shadow-md` | Default cards |
|
|
188
|
+
| `--shadow-lg` | Modals, dialogs |
|
|
189
|
+
| `--shadow-xl` | High emphasis elements |
|
|
190
|
+
|
|
191
|
+
```css
|
|
192
|
+
:root {
|
|
193
|
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
194
|
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
195
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
|
196
|
+
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Shadow Guidelines
|
|
201
|
+
|
|
202
|
+
- Use sparingly—most elements need no shadow
|
|
203
|
+
- Shadows indicate elevation/layering
|
|
204
|
+
- Cards on white backgrounds often need only subtle border OR shadow, not both
|
|
205
|
+
- Dark mode: reduce shadow opacity or use lighter shadows
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Z-Index Scale
|
|
210
|
+
|
|
211
|
+
| Token | Value | Use Case |
|
|
212
|
+
|-------|-------|----------|
|
|
213
|
+
| `--z-dropdown` | 50 | Dropdowns, popovers |
|
|
214
|
+
| `--z-sticky` | 100 | Sticky headers |
|
|
215
|
+
| `--z-modal` | 200 | Modal dialogs |
|
|
216
|
+
| `--z-toast` | 300 | Toast notifications |
|
|
217
|
+
| `--z-tooltip` | 400 | Tooltips |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Transitions
|
|
222
|
+
|
|
223
|
+
| Token | Duration | Easing | Use Case |
|
|
224
|
+
|-------|----------|--------|----------|
|
|
225
|
+
| `--duration-fast` | 100ms | ease-out | Hover states |
|
|
226
|
+
| `--duration-normal` | 200ms | ease-in-out | Default transitions |
|
|
227
|
+
| `--duration-slow` | 300ms | ease-in-out | Modals, overlays |
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
:root {
|
|
231
|
+
--duration-fast: 100ms;
|
|
232
|
+
--duration-normal: 200ms;
|
|
233
|
+
--duration-slow: 300ms;
|
|
234
|
+
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
|
235
|
+
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
236
|
+
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Motion Guidelines
|
|
241
|
+
|
|
242
|
+
- Respect `prefers-reduced-motion`
|
|
243
|
+
- Keep transitions under 300ms
|
|
244
|
+
- Use `transform` and `opacity` for performance
|
|
245
|
+
- Avoid animating `width`, `height`, `top`, `left`
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Breakpoints
|
|
250
|
+
|
|
251
|
+
| Token | Value | Description |
|
|
252
|
+
|-------|-------|-------------|
|
|
253
|
+
| `--screen-sm` | 640px | Small tablets |
|
|
254
|
+
| `--screen-md` | 768px | Tablets |
|
|
255
|
+
| `--screen-lg` | 1024px | Small laptops |
|
|
256
|
+
| `--screen-xl` | 1280px | Desktops |
|
|
257
|
+
| `--screen-2xl` | 1536px | Large screens |
|
|
258
|
+
|
|
259
|
+
### Media Query Pattern
|
|
260
|
+
|
|
261
|
+
```css
|
|
262
|
+
/* Mobile first */
|
|
263
|
+
.component { /* mobile styles */ }
|
|
264
|
+
|
|
265
|
+
@media (min-width: 768px) {
|
|
266
|
+
.component { /* tablet+ styles */ }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@media (min-width: 1024px) {
|
|
270
|
+
.component { /* desktop styles */ }
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Component Size Tokens
|
|
277
|
+
|
|
278
|
+
### Button Sizes
|
|
279
|
+
|
|
280
|
+
| Size | Height | Padding X | Font Size |
|
|
281
|
+
|------|--------|-----------|-----------|
|
|
282
|
+
| `sm` | 32px | 12px | 14px |
|
|
283
|
+
| `md` | 40px | 16px | 14px |
|
|
284
|
+
| `lg` | 48px | 24px | 16px |
|
|
285
|
+
|
|
286
|
+
### Input Sizes
|
|
287
|
+
|
|
288
|
+
| Size | Height | Padding X | Font Size |
|
|
289
|
+
|------|--------|-----------|-----------|
|
|
290
|
+
| `sm` | 32px | 12px | 14px |
|
|
291
|
+
| `md` | 40px | 14px | 16px |
|
|
292
|
+
| `lg` | 48px | 16px | 16px |
|
|
293
|
+
|
|
294
|
+
### Icon Sizes
|
|
295
|
+
|
|
296
|
+
| Token | Size | Use Case |
|
|
297
|
+
|-------|------|----------|
|
|
298
|
+
| `--icon-xs` | 12px | Inline with small text |
|
|
299
|
+
| `--icon-sm` | 16px | Inline with body text |
|
|
300
|
+
| `--icon-md` | 20px | Buttons, inputs |
|
|
301
|
+
| `--icon-lg` | 24px | Standalone icons |
|
|
302
|
+
| `--icon-xl` | 32px | Feature icons |
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Using Tokens in Practice
|
|
307
|
+
|
|
308
|
+
### Example: Card Component
|
|
309
|
+
|
|
310
|
+
```css
|
|
311
|
+
.card {
|
|
312
|
+
background: var(--neutral-50);
|
|
313
|
+
border: 1px solid var(--neutral-200);
|
|
314
|
+
border-radius: var(--radius-lg);
|
|
315
|
+
padding: var(--space-6);
|
|
316
|
+
box-shadow: var(--shadow-sm);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.card-title {
|
|
320
|
+
font-size: var(--text-xl);
|
|
321
|
+
font-weight: var(--font-semibold);
|
|
322
|
+
color: var(--neutral-900);
|
|
323
|
+
margin-bottom: var(--space-2);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.card-body {
|
|
327
|
+
font-size: var(--text-base);
|
|
328
|
+
color: var(--neutral-700);
|
|
329
|
+
line-height: var(--leading-relaxed);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Example: Button Component
|
|
334
|
+
|
|
335
|
+
```css
|
|
336
|
+
.btn {
|
|
337
|
+
height: 40px;
|
|
338
|
+
padding: 0 var(--space-4);
|
|
339
|
+
font-size: var(--text-sm);
|
|
340
|
+
font-weight: var(--font-semibold);
|
|
341
|
+
border-radius: var(--radius-md);
|
|
342
|
+
transition: all var(--duration-fast) var(--ease-default);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.btn-primary {
|
|
346
|
+
background: var(--primary-600);
|
|
347
|
+
color: white;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.btn-primary:hover {
|
|
351
|
+
background: var(--primary-700);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.btn-secondary {
|
|
355
|
+
background: transparent;
|
|
356
|
+
border: 1px solid var(--neutral-300);
|
|
357
|
+
color: var(--neutral-700);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard API endpoint tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
const TEST_DIR = `/tmp/flux-dashboard-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
9
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
10
|
+
|
|
11
|
+
import { clearAdapterCache } from "../../server/adapters/factory.js";
|
|
12
|
+
import { getAdapter } from "../../server/adapters/index.js";
|
|
13
|
+
import { initDb } from "../../server/db/index.js";
|
|
14
|
+
import { startDashboard } from "../server.js";
|
|
15
|
+
|
|
16
|
+
let dashboardUrl: string;
|
|
17
|
+
let stopDashboard: () => void;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
mkdirSync(`${TEST_DIR}/.flux/prds`, { recursive: true });
|
|
21
|
+
|
|
22
|
+
writeFileSync(
|
|
23
|
+
`${TEST_DIR}/.flux/project.json`,
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
name: "test-project",
|
|
26
|
+
vision: "Test vision",
|
|
27
|
+
ref_prefix: "TEST",
|
|
28
|
+
project_root: TEST_DIR,
|
|
29
|
+
created_at: new Date().toISOString(),
|
|
30
|
+
adapter: { type: "local" },
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
initDb();
|
|
35
|
+
clearAdapterCache();
|
|
36
|
+
|
|
37
|
+
const adapter = getAdapter();
|
|
38
|
+
const prd = await adapter.createPrd({
|
|
39
|
+
title: "Test PRD",
|
|
40
|
+
description: "Test description",
|
|
41
|
+
tag: "test-tag",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const epic = await adapter.createEpic({
|
|
45
|
+
prdRef: prd.ref,
|
|
46
|
+
title: "Test Epic",
|
|
47
|
+
description: "Epic description",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await adapter.createTask({
|
|
51
|
+
epicRef: epic.ref,
|
|
52
|
+
title: "Test Task",
|
|
53
|
+
description: "Task description",
|
|
54
|
+
priority: "HIGH",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { port, stop } = await startDashboard();
|
|
58
|
+
dashboardUrl = `http://localhost:${port}`;
|
|
59
|
+
stopDashboard = stop;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
stopDashboard();
|
|
64
|
+
clearAdapterCache();
|
|
65
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("Dashboard API", () => {
|
|
69
|
+
describe("GET /api/tree", () => {
|
|
70
|
+
test("returns hierarchical tree structure", async () => {
|
|
71
|
+
const response = await fetch(`${dashboardUrl}/api/tree`);
|
|
72
|
+
expect(response.status).toBe(200);
|
|
73
|
+
|
|
74
|
+
const tree = await response.json();
|
|
75
|
+
expect(Array.isArray(tree)).toBe(true);
|
|
76
|
+
expect(tree.length).toBeGreaterThan(0);
|
|
77
|
+
|
|
78
|
+
const prd = tree[0];
|
|
79
|
+
expect(prd.ref).toMatch(/^TEST-P\d+$/);
|
|
80
|
+
expect(prd.title).toBe("Test PRD");
|
|
81
|
+
expect(prd.epics).toBeDefined();
|
|
82
|
+
expect(prd.epics.length).toBe(1);
|
|
83
|
+
|
|
84
|
+
const epic = prd.epics[0];
|
|
85
|
+
expect(epic.ref).toMatch(/^TEST-E\d+$/);
|
|
86
|
+
expect(epic.tasks).toBeDefined();
|
|
87
|
+
expect(epic.tasks.length).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("GET /api/prd/:ref", () => {
|
|
92
|
+
test("returns PRD details", async () => {
|
|
93
|
+
const treeResponse = await fetch(`${dashboardUrl}/api/tree`);
|
|
94
|
+
const tree = await treeResponse.json();
|
|
95
|
+
const prdRef = tree[0].ref;
|
|
96
|
+
|
|
97
|
+
const response = await fetch(`${dashboardUrl}/api/prd/${prdRef}`);
|
|
98
|
+
expect(response.status).toBe(200);
|
|
99
|
+
|
|
100
|
+
const prd = await response.json();
|
|
101
|
+
expect(prd.ref).toBe(prdRef);
|
|
102
|
+
expect(prd.title).toBe("Test PRD");
|
|
103
|
+
expect(prd.description).toBe("Test description");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("returns 404 for non-existent PRD", async () => {
|
|
107
|
+
const response = await fetch(`${dashboardUrl}/api/prd/TEST-P999`);
|
|
108
|
+
expect(response.status).toBe(404);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("GET /api/epic/:ref", () => {
|
|
113
|
+
test("returns epic details with criteria", async () => {
|
|
114
|
+
const treeResponse = await fetch(`${dashboardUrl}/api/tree`);
|
|
115
|
+
const tree = await treeResponse.json();
|
|
116
|
+
const epicRef = tree[0].epics[0].ref;
|
|
117
|
+
|
|
118
|
+
const response = await fetch(`${dashboardUrl}/api/epic/${epicRef}`);
|
|
119
|
+
expect(response.status).toBe(200);
|
|
120
|
+
|
|
121
|
+
const epic = await response.json();
|
|
122
|
+
expect(epic.ref).toBe(epicRef);
|
|
123
|
+
expect(epic.title).toBe("Test Epic");
|
|
124
|
+
expect(epic.criteria).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("returns 404 for non-existent epic", async () => {
|
|
128
|
+
const response = await fetch(`${dashboardUrl}/api/epic/TEST-E999`);
|
|
129
|
+
expect(response.status).toBe(404);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("GET /api/task/:ref", () => {
|
|
134
|
+
test("returns task details with criteria and dependencies", async () => {
|
|
135
|
+
const treeResponse = await fetch(`${dashboardUrl}/api/tree`);
|
|
136
|
+
const tree = await treeResponse.json();
|
|
137
|
+
const taskRef = tree[0].epics[0].tasks[0].ref;
|
|
138
|
+
|
|
139
|
+
const response = await fetch(`${dashboardUrl}/api/task/${taskRef}`);
|
|
140
|
+
expect(response.status).toBe(200);
|
|
141
|
+
|
|
142
|
+
const task = await response.json();
|
|
143
|
+
expect(task.ref).toBe(taskRef);
|
|
144
|
+
expect(task.title).toBe("Test Task");
|
|
145
|
+
expect(task.priority).toBe("HIGH");
|
|
146
|
+
expect(task.criteria).toBeDefined();
|
|
147
|
+
expect(task.dependencies).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("returns 404 for non-existent task", async () => {
|
|
151
|
+
const response = await fetch(`${dashboardUrl}/api/task/TEST-T999`);
|
|
152
|
+
expect(response.status).toBe(404);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("GET /api/tags", () => {
|
|
157
|
+
test("returns tag list with counts", async () => {
|
|
158
|
+
const response = await fetch(`${dashboardUrl}/api/tags`);
|
|
159
|
+
expect(response.status).toBe(200);
|
|
160
|
+
|
|
161
|
+
const tags = await response.json();
|
|
162
|
+
expect(Array.isArray(tags)).toBe(true);
|
|
163
|
+
|
|
164
|
+
const allTag = tags.find((t: { tag: string }) => t.tag === "All");
|
|
165
|
+
expect(allTag).toBeDefined();
|
|
166
|
+
expect(allTag.count).toBeGreaterThan(0);
|
|
167
|
+
|
|
168
|
+
const testTag = tags.find((t: { tag: string }) => t.tag === "test-tag");
|
|
169
|
+
expect(testTag).toBeDefined();
|
|
170
|
+
expect(testTag.count).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("GET /api/dependencies", () => {
|
|
175
|
+
test("returns dependency edges", async () => {
|
|
176
|
+
const response = await fetch(`${dashboardUrl}/api/dependencies`);
|
|
177
|
+
expect(response.status).toBe(200);
|
|
178
|
+
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
expect(data.edges).toBeDefined();
|
|
181
|
+
expect(Array.isArray(data.edges)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("Static files", () => {
|
|
186
|
+
test("serves index.html at root", async () => {
|
|
187
|
+
const response = await fetch(`${dashboardUrl}/`);
|
|
188
|
+
expect(response.status).toBe(200);
|
|
189
|
+
expect(response.headers.get("content-type")).toBe("text/html");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("serves CSS files", async () => {
|
|
193
|
+
const response = await fetch(`${dashboardUrl}/styles.css`);
|
|
194
|
+
expect(response.status).toBe(200);
|
|
195
|
+
expect(response.headers.get("content-type")).toBe("text/css");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("serves JS files", async () => {
|
|
199
|
+
const response = await fetch(`${dashboardUrl}/app.js`);
|
|
200
|
+
expect(response.status).toBe(200);
|
|
201
|
+
expect(response.headers.get("content-type")).toBe(
|
|
202
|
+
"application/javascript",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("returns 404 for non-existent files", async () => {
|
|
207
|
+
const response = await fetch(`${dashboardUrl}/nonexistent.txt`);
|
|
208
|
+
expect(response.status).toBe(404);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform Browser Opening
|
|
3
|
+
*
|
|
4
|
+
* Opens the default browser to the dashboard URL on macOS, Linux, and Windows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export async function openBrowser(url: string): Promise<void> {
|
|
8
|
+
const platform = process.platform;
|
|
9
|
+
|
|
10
|
+
let command: string[];
|
|
11
|
+
|
|
12
|
+
switch (platform) {
|
|
13
|
+
case "darwin":
|
|
14
|
+
command = ["open", url];
|
|
15
|
+
break;
|
|
16
|
+
case "win32":
|
|
17
|
+
command = ["cmd", "/c", "start", url];
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
// Linux and others
|
|
21
|
+
command = ["xdg-open", url];
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(command, {
|
|
27
|
+
stdout: "ignore",
|
|
28
|
+
stderr: "ignore",
|
|
29
|
+
});
|
|
30
|
+
await proc.exited;
|
|
31
|
+
} catch {
|
|
32
|
+
console.log(`Could not open browser automatically.`);
|
|
33
|
+
console.log(`Please open manually: ${url}`);
|
|
34
|
+
}
|
|
35
|
+
}
|