@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.
Files changed (37) hide show
  1. package/agents/coder.md +150 -25
  2. package/commands/breakdown.md +44 -7
  3. package/commands/implement.md +165 -15
  4. package/commands/prd.md +176 -1
  5. package/manifest.json +2 -1
  6. package/package.json +4 -2
  7. package/skills/prd-writer/SKILL.md +184 -0
  8. package/skills/ux-ui-design/SKILL.md +346 -0
  9. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  10. package/src/dashboard/__tests__/api.test.ts +211 -0
  11. package/src/dashboard/browser.ts +35 -0
  12. package/src/dashboard/public/app.js +869 -0
  13. package/src/dashboard/public/index.html +90 -0
  14. package/src/dashboard/public/styles.css +807 -0
  15. package/src/dashboard/public/vendor/highlight.css +10 -0
  16. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  17. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  18. package/src/dashboard/server.ts +296 -0
  19. package/src/dashboard/watchers.ts +83 -0
  20. package/src/server/adapters/__tests__/dependency-ops.test.ts +52 -18
  21. package/src/server/adapters/linear/adapter.ts +19 -14
  22. package/src/server/adapters/local-adapter.ts +48 -7
  23. package/src/server/db/__tests__/queries.test.ts +2 -1
  24. package/src/server/db/schema.ts +9 -0
  25. package/src/server/tools/__tests__/crud.test.ts +111 -1
  26. package/src/server/tools/__tests__/mcp-interface.test.ts +2 -1
  27. package/src/server/tools/__tests__/query.test.ts +73 -2
  28. package/src/server/tools/__tests__/z-configure-linear.test.ts +1 -1
  29. package/src/server/tools/__tests__/z-get-linear-url.test.ts +1 -1
  30. package/src/server/tools/create-epic.ts +11 -2
  31. package/src/server/tools/create-prd.ts +11 -2
  32. package/src/server/tools/create-task.ts +11 -2
  33. package/src/server/tools/dependencies.ts +2 -2
  34. package/src/server/tools/get-entity.ts +12 -10
  35. package/src/server/tools/render-status.ts +38 -20
  36. package/src/status-line/__tests__/status-line.test.ts +1 -1
  37. 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
+ }