@chriscode/devmux 1.2.0 → 1.3.1

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.
@@ -0,0 +1,437 @@
1
+ import {
2
+ getAllStatus,
3
+ init_loader,
4
+ loadConfig
5
+ } from "./chunk-ALENFKSX.js";
6
+ import {
7
+ __export,
8
+ init_esm_shims
9
+ } from "./chunk-66UOCF5R.js";
10
+
11
+ // src/dashboard/index.ts
12
+ var dashboard_exports = {};
13
+ __export(dashboard_exports, {
14
+ startDashboard: () => startDashboard
15
+ });
16
+ init_esm_shims();
17
+
18
+ // src/dashboard/server.ts
19
+ init_esm_shims();
20
+ init_loader();
21
+ import { exec } from "child_process";
22
+ import { existsSync } from "fs";
23
+ import { createServer } from "http";
24
+
25
+ // src/dashboard/template.ts
26
+ init_esm_shims();
27
+ function renderDashboard(data) {
28
+ return `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="UTF-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
33
+ <title>${data.project}</title>
34
+ <style>
35
+ :root {
36
+ --bg: #111;
37
+ --bar-bg: #1a1a1a;
38
+ --text: #ccc;
39
+ --text-muted: #666;
40
+ --border: #222;
41
+ --accent: #007acc;
42
+ --success: #4caf50;
43
+ --error: #f44336;
44
+ --muted: #444;
45
+ }
46
+
47
+ * { box-sizing: border-box; margin: 0; padding: 0; }
48
+
49
+ body {
50
+ background: var(--bg);
51
+ color: var(--text);
52
+ font: 11px/1 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
53
+ height: 100vh;
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ }
58
+
59
+ .content-area { flex: 1; position: relative; background: #fff; }
60
+
61
+ iframe {
62
+ position: absolute;
63
+ top: 0; left: 0; width: 100%; height: 100%;
64
+ border: none;
65
+ background: white;
66
+ display: none;
67
+ }
68
+
69
+ iframe.active { display: block; }
70
+
71
+ .empty-state {
72
+ position: absolute;
73
+ top: 0; left: 0; width: 100%; height: 100%;
74
+ background: var(--bg);
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ color: var(--text-muted);
79
+ font-size: 12px;
80
+ }
81
+
82
+ nav {
83
+ height: 28px;
84
+ background: var(--bar-bg);
85
+ border-top: 1px solid var(--border);
86
+ display: flex;
87
+ align-items: stretch;
88
+ flex-shrink: 0;
89
+ padding: 0;
90
+ }
91
+
92
+ .bar-info {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 6px;
96
+ padding: 0 10px;
97
+ min-width: 0;
98
+ overflow: hidden;
99
+ border-right: 1px solid var(--border);
100
+ flex-shrink: 1;
101
+ }
102
+
103
+ .bar-info-label {
104
+ color: var(--text-muted);
105
+ font-size: 10px;
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ .bar-info-url {
110
+ color: var(--text-muted);
111
+ font-size: 10px;
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ white-space: nowrap;
115
+ opacity: 0.7;
116
+ user-select: all;
117
+ }
118
+
119
+ .bar-spacer { flex: 1; }
120
+
121
+ .bar-tabs {
122
+ display: flex;
123
+ align-items: stretch;
124
+ overflow-x: auto;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .tab {
129
+ padding: 0 8px;
130
+ cursor: pointer;
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 5px;
134
+ border-left: 1px solid var(--border);
135
+ transition: background 100ms;
136
+ white-space: nowrap;
137
+ flex-shrink: 0;
138
+ position: relative;
139
+ }
140
+
141
+ .tab:hover { background: #222; }
142
+ .tab.active { background: var(--accent); color: #fff; }
143
+
144
+ .tab-port {
145
+ font-size: 9px;
146
+ color: var(--text-muted);
147
+ opacity: 0.7;
148
+ }
149
+ .tab.active .tab-port { color: rgba(255,255,255,0.6); }
150
+
151
+ .dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
152
+ .dot.healthy { background: var(--success); }
153
+ .dot.unhealthy { background: var(--error); }
154
+ .dot.no-check { background: var(--muted); }
155
+
156
+ .tooltip {
157
+ display: none;
158
+ position: absolute;
159
+ bottom: calc(100% + 6px);
160
+ left: 50%;
161
+ transform: translateX(-50%);
162
+ background: #2a2a2a;
163
+ border: 1px solid #3a3a3a;
164
+ border-radius: 4px;
165
+ padding: 6px 8px;
166
+ font-size: 10px;
167
+ line-height: 1.5;
168
+ white-space: nowrap;
169
+ color: var(--text);
170
+ pointer-events: none;
171
+ z-index: 100;
172
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
173
+ }
174
+
175
+ .tooltip::after {
176
+ content: '';
177
+ position: absolute;
178
+ top: 100%;
179
+ left: 50%;
180
+ transform: translateX(-50%);
181
+ border: 4px solid transparent;
182
+ border-top-color: #3a3a3a;
183
+ }
184
+
185
+ .tab:hover .tooltip { display: block; }
186
+
187
+ .tooltip-row {
188
+ display: flex;
189
+ gap: 6px;
190
+ }
191
+
192
+ .tooltip-key { color: var(--text-muted); }
193
+
194
+ .hidden { display: none !important; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+
199
+ <div id="content" class="content-area">
200
+ <div id="empty-state" class="empty-state">Select a service</div>
201
+ </div>
202
+
203
+ <nav id="bar">
204
+ <div id="bar-info" class="bar-info">
205
+ <span id="bar-info-label" class="bar-info-label"></span>
206
+ <span id="bar-info-url" class="bar-info-url"></span>
207
+ </div>
208
+ <div class="bar-spacer"></div>
209
+ <div id="bar-tabs" class="bar-tabs"></div>
210
+ </nav>
211
+
212
+ <script>
213
+ window.__DEVMUX__ = ${JSON.stringify(data)};
214
+ </script>
215
+
216
+ <script>
217
+ (function() {
218
+ const data = window.__DEVMUX__;
219
+ let selected = null;
220
+ const services = data.services || [];
221
+ const iframeCache = new Map();
222
+
223
+ const barTabs = document.getElementById('bar-tabs');
224
+ const barInfoLabel = document.getElementById('bar-info-label');
225
+ const barInfoUrl = document.getElementById('bar-info-url');
226
+ const content = document.getElementById('content');
227
+ const emptyState = document.getElementById('empty-state');
228
+
229
+ function init() {
230
+ renderBar();
231
+ if (services.length > 0) {
232
+ const first = services.find(s => s.resolvedPort || s.port) || services[0];
233
+ selectService(first.name);
234
+ }
235
+ startPolling();
236
+ }
237
+
238
+ function getServiceUrl(s) {
239
+ const port = s.resolvedPort || s.port;
240
+ return port ? 'http://localhost:' + port : null;
241
+ }
242
+
243
+ function renderBar() {
244
+ barTabs.innerHTML = '';
245
+ services.forEach(s => {
246
+ const tab = document.createElement('div');
247
+ tab.className = 'tab' + (selected === s.name ? ' active' : '');
248
+ tab.onclick = () => selectService(s.name);
249
+
250
+ let dotClass = 'no-check';
251
+ if (s.hasHealthCheck) dotClass = s.healthy ? 'healthy' : 'unhealthy';
252
+
253
+ const port = s.resolvedPort || s.port;
254
+ const portLabel = port ? '<span class="tab-port">:' + port + '</span>' : '';
255
+
256
+ const tooltipRows = [];
257
+ tooltipRows.push('<div class="tooltip-row"><span class="tooltip-key">service</span> ' + s.name + '</div>');
258
+ if (port) tooltipRows.push('<div class="tooltip-row"><span class="tooltip-key">port</span> ' + port + '</div>');
259
+ tooltipRows.push('<div class="tooltip-row"><span class="tooltip-key">status</span> ' + (s.hasHealthCheck ? (s.healthy ? 'healthy' : 'unhealthy') : 'no check') + '</div>');
260
+ if (port) tooltipRows.push('<div class="tooltip-row"><span class="tooltip-key">url</span> http://localhost:' + port + '</div>');
261
+
262
+ tab.innerHTML =
263
+ '<div class="dot ' + dotClass + '"></div>' +
264
+ s.name + portLabel +
265
+ '<div class="tooltip">' + tooltipRows.join('') + '</div>';
266
+
267
+ barTabs.appendChild(tab);
268
+ });
269
+ }
270
+
271
+ function updateBarInfo() {
272
+ const service = services.find(s => s.name === selected);
273
+ if (!service) {
274
+ barInfoLabel.textContent = '';
275
+ barInfoUrl.textContent = '';
276
+ return;
277
+ }
278
+ barInfoLabel.textContent = service.name;
279
+ const url = getServiceUrl(service);
280
+ barInfoUrl.textContent = url || '(no port)';
281
+ }
282
+
283
+ function selectService(name) {
284
+ selected = name;
285
+ const service = services.find(s => s.name === name);
286
+ if (!service) return;
287
+
288
+ barTabs.querySelectorAll('.tab').forEach((tab, i) => {
289
+ tab.classList.toggle('active', services[i].name === name);
290
+ });
291
+
292
+ updateBarInfo();
293
+
294
+ iframeCache.forEach(iframe => iframe.classList.remove('active'));
295
+
296
+ const port = service.resolvedPort || service.port;
297
+ if (!port) {
298
+ emptyState.classList.remove('hidden');
299
+ emptyState.textContent = name + ' (no port)';
300
+ return;
301
+ }
302
+
303
+ emptyState.classList.add('hidden');
304
+
305
+ let iframe = iframeCache.get(name);
306
+ if (!iframe) {
307
+ iframe = document.createElement('iframe');
308
+ iframe.src = 'http://localhost:' + port;
309
+ iframe.id = 'frame-' + name;
310
+ content.appendChild(iframe);
311
+ iframeCache.set(name, iframe);
312
+ }
313
+
314
+ iframe.classList.add('active');
315
+ }
316
+
317
+ function startPolling() {
318
+ const poll = () => setTimeout(async () => {
319
+ try {
320
+ const res = await fetch('/api/status');
321
+ const newData = await res.json();
322
+ services.length = 0;
323
+ services.push(...newData.services);
324
+ renderBar();
325
+ updateBarInfo();
326
+ } catch (e) {
327
+ console.error('Poll error:', e);
328
+ } finally {
329
+ poll();
330
+ }
331
+ }, 5000);
332
+ poll();
333
+ }
334
+
335
+ init();
336
+ })();
337
+ </script>
338
+ </body>
339
+ </html>`;
340
+ }
341
+
342
+ // src/dashboard/server.ts
343
+ function getConfigPath(config) {
344
+ const names = ["devmux.config.json", ".devmuxrc.json", ".devmuxrc"];
345
+ for (const name of names) {
346
+ const path = `${config.configRoot}/${name}`;
347
+ if (existsSync(path)) return path;
348
+ }
349
+ return `${config.configRoot}/devmux.config.json`;
350
+ }
351
+ function statusToService(s, config) {
352
+ const def = config.services[s.name];
353
+ if (def?.dashboard === false) return null;
354
+ if (def?.dashboard !== true && !s.port && !s.resolvedPort) return null;
355
+ return {
356
+ name: s.name,
357
+ healthy: s.healthy,
358
+ port: s.port,
359
+ resolvedPort: s.resolvedPort,
360
+ hasHealthCheck: def?.health !== void 0
361
+ };
362
+ }
363
+ function openBrowser(url) {
364
+ const command = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `cmd /c start "${url}"` : `xdg-open "${url}"`;
365
+ exec(command, () => {
366
+ });
367
+ }
368
+ function startDashboard(options = {}) {
369
+ const port = options.port ?? 9e3;
370
+ const shouldOpen = options.open ?? true;
371
+ const config = loadConfig();
372
+ const configPath = getConfigPath(config);
373
+ const server = createServer(async (req, res) => {
374
+ if (req.url === "/api/status") {
375
+ try {
376
+ const statuses = await getAllStatus(config);
377
+ const services = statuses.map((s) => statusToService(s, config)).filter((s) => s !== null);
378
+ const payload = {
379
+ project: config.project,
380
+ instanceId: config.instanceId || null,
381
+ configPath,
382
+ services
383
+ };
384
+ res.writeHead(200, {
385
+ "Content-Type": "application/json",
386
+ "Cache-Control": "no-cache"
387
+ });
388
+ res.end(JSON.stringify(payload));
389
+ } catch (err) {
390
+ res.writeHead(500, { "Content-Type": "application/json" });
391
+ res.end(JSON.stringify({ error: "Failed to fetch status" }));
392
+ }
393
+ return;
394
+ }
395
+ if (req.url === "/" || req.url === "/index.html") {
396
+ try {
397
+ const statuses = await getAllStatus(config);
398
+ const services = statuses.map((s) => statusToService(s, config)).filter((s) => s !== null);
399
+ const data = {
400
+ project: config.project,
401
+ instanceId: config.instanceId || "",
402
+ configPath,
403
+ dashboardPort: port,
404
+ services
405
+ };
406
+ res.writeHead(200, {
407
+ "Content-Type": "text/html; charset=utf-8",
408
+ "Cache-Control": "no-cache"
409
+ });
410
+ res.end(renderDashboard(data));
411
+ } catch (err) {
412
+ res.writeHead(500, { "Content-Type": "text/plain" });
413
+ res.end("Failed to render dashboard");
414
+ }
415
+ return;
416
+ }
417
+ res.writeHead(404, { "Content-Type": "text/plain" });
418
+ res.end("Not Found");
419
+ });
420
+ server.listen(port, () => {
421
+ const url = `http://localhost:${port}`;
422
+ console.log(`devmux dashboard running at ${url}`);
423
+ console.log(` Project: ${config.project}`);
424
+ console.log(` Config: ${configPath}`);
425
+ console.log("");
426
+ console.log("Press Ctrl+C to stop.");
427
+ if (shouldOpen) {
428
+ openBrowser(url);
429
+ }
430
+ });
431
+ return server;
432
+ }
433
+
434
+ export {
435
+ startDashboard,
436
+ dashboard_exports
437
+ };