devglide 0.1.2 → 0.1.4

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 (38) hide show
  1. package/package.json +5 -1
  2. package/src/apps/kanban/src/index.ts +1 -1
  3. package/src/apps/log/.turbo/turbo-lint.log +2 -2
  4. package/src/apps/log/src/index.ts +1 -1
  5. package/src/apps/prompts/.turbo/turbo-lint.log +3 -4
  6. package/src/apps/prompts/src/index.ts +1 -1
  7. package/src/apps/shell/.turbo/turbo-lint.log +5 -5
  8. package/src/apps/shell/src/index.ts +1 -1
  9. package/src/apps/test/.turbo/turbo-lint.log +2 -2
  10. package/src/apps/test/src/index.ts +1 -1
  11. package/src/apps/vocabulary/.turbo/turbo-lint.log +3 -4
  12. package/src/apps/vocabulary/src/index.ts +1 -1
  13. package/src/apps/voice/.turbo/turbo-lint.log +2 -2
  14. package/src/apps/voice/src/index.ts +1 -1
  15. package/src/apps/workflow/.turbo/turbo-lint.log +3 -4
  16. package/src/apps/workflow/src/index.ts +1 -1
  17. package/src/project-context.ts +36 -0
  18. package/src/public/app.js +701 -0
  19. package/src/public/favicon.svg +7 -0
  20. package/src/public/index.html +78 -0
  21. package/src/public/state.js +84 -0
  22. package/src/public/style.css +1213 -0
  23. package/src/routers/coder.ts +157 -0
  24. package/src/routers/dashboard.ts +158 -0
  25. package/src/routers/kanban.ts +38 -0
  26. package/src/routers/log.ts +42 -0
  27. package/src/routers/prompts.ts +134 -0
  28. package/src/routers/shell/index.ts +47 -0
  29. package/src/routers/shell/pty-manager.ts +107 -0
  30. package/src/routers/shell/shell-config.ts +38 -0
  31. package/src/routers/shell/shell-routes.ts +108 -0
  32. package/src/routers/shell/shell-socket.ts +321 -0
  33. package/src/routers/shell/shell-state.ts +59 -0
  34. package/src/routers/test.ts +254 -0
  35. package/src/routers/vocabulary.ts +149 -0
  36. package/src/routers/voice.ts +10 -0
  37. package/src/routers/workflow.ts +243 -0
  38. package/src/server.ts +325 -0
package/src/server.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Devglide Unified Server
3
+ *
4
+ * Consolidates all 9 Devglide micro-services into a single Express/Socket.io
5
+ * server. Each app's routes live in src/routers/<app>.ts and are mounted under
6
+ * /api/<app>. Static assets are served from the original app public dirs.
7
+ */
8
+
9
+ import express from 'express';
10
+ import { createServer } from 'http';
11
+ import { Server, type Namespace } from 'socket.io';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ // Shared packages
16
+ import { isLocalhostOrigin } from './packages/auth-middleware.js';
17
+ import { LOGS_DIR } from './packages/paths.js';
18
+ import { snifferSource, runnerSource } from './packages/devtools-middleware.js';
19
+ import { initServerSniffer } from './packages/server-sniffer.js';
20
+ import { mountMcpHttp } from './packages/mcp-utils/src/index.js';
21
+
22
+ // Project context
23
+ import { getActiveProject, setActiveProject } from './project-context.js';
24
+
25
+ // Initial stored project
26
+ import { getActiveProject as getStoredProject } from './packages/project-store.js';
27
+
28
+ // Routers
29
+ import { router as dashboardRouter, initDashboard } from './routers/dashboard.js';
30
+ import { router as kanbanRouter, createKanbanMcpServer } from './routers/kanban.js';
31
+ import { router as logRouter, initLog, shutdownLog, createLogMcpServer } from './routers/log.js';
32
+ import { recordSession } from './apps/log/src/routes/log.js';
33
+ import { router as testRouter, initTest, shutdownTest, createTestMcpServer } from './routers/test.js';
34
+ import { router as shellRouter, initShell, mountShellMcp, shutdownShell } from './routers/shell/index.js';
35
+ import { router as coderRouter } from './routers/coder.js';
36
+ import { router as workflowRouter, initWorkflow, shutdownWorkflow, createWorkflowMcpServer } from './routers/workflow.js';
37
+ import { router as voiceRouter, createVoiceMcpServer } from './routers/voice.js';
38
+ import { router as vocabularyRouter, createVocabularyMcpServer } from './routers/vocabulary.js';
39
+ import { router as promptsRouter, createPromptsMcpServer } from './routers/prompts.js';
40
+
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Constants
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const __filename = fileURLToPath(import.meta.url);
47
+ const __dirname = path.dirname(__filename);
48
+ const ROOT = path.resolve(__dirname, '..');
49
+ const PORT = parseInt(process.env.PORT || '7000', 10);
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Server sniffer — captures server-side console output to disk
53
+ // ---------------------------------------------------------------------------
54
+
55
+ initServerSniffer({ service: 'devglide', targetPath: path.join(ROOT, 'server.log'), logPort: PORT });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Express + HTTP + Socket.io
59
+ // ---------------------------------------------------------------------------
60
+
61
+ const app = express();
62
+ const httpServer = createServer(app);
63
+
64
+ const io = new Server(httpServer, {
65
+ cors: {
66
+ origin: (origin: string | undefined, cb: (err: Error | null, allow?: boolean) => void) => {
67
+ if (!origin || isLocalhostOrigin(origin)) return cb(null, true);
68
+ cb(null, false);
69
+ },
70
+ },
71
+ });
72
+
73
+ // No auth — local-only dev tool; CORS restricts cross-origin access.
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Security headers
77
+ // ---------------------------------------------------------------------------
78
+
79
+ app.use((_req, res, next) => {
80
+ res.setHeader('X-Content-Type-Options', 'nosniff');
81
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
82
+ res.setHeader('X-XSS-Protection', '0');
83
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
84
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(self), geolocation=()');
85
+ next();
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Shared CORS middleware
90
+ // ---------------------------------------------------------------------------
91
+
92
+ app.use((req, res, next) => {
93
+ const origin = req.headers.origin;
94
+ if (origin && isLocalhostOrigin(origin)) {
95
+ res.setHeader('Access-Control-Allow-Origin', origin);
96
+ }
97
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
98
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
99
+ if (req.method === 'OPTIONS') return res.sendStatus(204);
100
+ next();
101
+ });
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Body parser — 1 MB default, 25 MB for voice transcription uploads
105
+ // ---------------------------------------------------------------------------
106
+
107
+ const jsonDefault = express.json({ limit: '1mb' });
108
+ const jsonLarge = express.json({ limit: '25mb' });
109
+
110
+ app.use((req, res, next) => {
111
+ const isVoiceUpload = req.path.startsWith('/api/voice/transcribe') || req.path === '/api/transcribe';
112
+ (isVoiceUpload ? jsonLarge : jsonDefault)(req, res, next);
113
+ });
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Static file serving
117
+ // ---------------------------------------------------------------------------
118
+
119
+ app.use('/shared-assets', express.static(path.join(ROOT, 'src/packages/shared-assets')));
120
+ app.use('/df', express.static(path.join(ROOT, 'src/packages/design-tokens/dist')));
121
+ app.use('/design-tokens', express.static(path.join(ROOT, 'src/packages/design-tokens/dist')));
122
+
123
+ // App-specific static dirs
124
+ app.use('/app/kanban', express.static(path.join(ROOT, 'src/apps/kanban/public')));
125
+ app.use('/app/log', express.static(path.join(ROOT, 'src/apps/log/public')));
126
+ app.use('/app/test', express.static(path.join(ROOT, 'src/apps/test/public')));
127
+ app.use('/app/shell', express.static(path.join(ROOT, 'src/apps/shell/public')));
128
+ app.use('/app/coder', express.static(path.join(ROOT, 'src/apps/coder/public')));
129
+ app.use('/app/workflow', express.static(path.join(ROOT, 'src/apps/workflow/public')));
130
+ app.use('/app/voice', express.static(path.join(ROOT, 'src/apps/voice/public')));
131
+ app.use('/app/vocabulary', express.static(path.join(ROOT, 'src/apps/vocabulary/public')));
132
+ app.use('/app/keymap', express.static(path.join(ROOT, 'src/apps/keymap/public')));
133
+ app.use('/app/prompts', express.static(path.join(ROOT, 'src/apps/prompts/public')));
134
+ app.use('/app/documentation', express.static(path.join(ROOT, 'src/apps/documentation/public')));
135
+
136
+ // App shell (unified SPA) is the default landing page at root
137
+ app.use('/', express.static(path.join(ROOT, 'src/public')));
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Rate limiter — per-IP request throttling for sensitive endpoints
141
+ // ---------------------------------------------------------------------------
142
+
143
+ const rateLimitState = new Map<string, { count: number; resetAt: number }>();
144
+
145
+ function rateLimit(maxRequests: number, windowMs: number) {
146
+ return (req: express.Request, res: express.Response, next: express.NextFunction) => {
147
+ const ip = req.ip ?? 'unknown';
148
+ const now = Date.now();
149
+ let entry = rateLimitState.get(ip);
150
+ if (!entry || now > entry.resetAt) {
151
+ entry = { count: 0, resetAt: now + windowMs };
152
+ rateLimitState.set(ip, entry);
153
+ }
154
+ entry.count++;
155
+ if (entry.count > maxRequests) {
156
+ res.status(429).json({ error: 'Too many requests' });
157
+ return;
158
+ }
159
+ next();
160
+ };
161
+ }
162
+
163
+ // Clean up stale entries periodically
164
+ setInterval(() => {
165
+ const now = Date.now();
166
+ for (const [key, entry] of rateLimitState) {
167
+ if (now > entry.resetAt) rateLimitState.delete(key);
168
+ }
169
+ }, 60_000);
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // No auth middleware — local-only dev tool; CORS handles access control.
173
+ // ---------------------------------------------------------------------------
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Devtools — serves /__devtools.js with inlined sniffer + runner.
177
+ // Placed after auth to prevent unauthenticated path disclosure.
178
+ // ---------------------------------------------------------------------------
179
+
180
+ app.get('/__devtools.js', (_req, res) => {
181
+ let script = `window.__devglideSnifferConfig=${JSON.stringify({
182
+ serverOrigin: `http://localhost:${PORT}`,
183
+ targetPath: path.join(LOGS_DIR, 'devglide-console.log'),
184
+ persistent: true,
185
+ allowedTypes: {},
186
+ })};\n`;
187
+
188
+ script += `window.__devglideRunnerConfig=${JSON.stringify({
189
+ serverOrigin: `http://localhost:${PORT}`,
190
+ target: ROOT,
191
+ })};\n`;
192
+
193
+ script += snifferSource + '\n' + runnerSource;
194
+ res.type('application/javascript').send(script);
195
+ });
196
+
197
+ app.get('/devtools.js', (req, res) => {
198
+ const dir = (req.query.target as string) || getActiveProject()?.path;
199
+ if (!dir) {
200
+ return res.type('application/javascript').send('/* devtools: no target */');
201
+ }
202
+
203
+ let script = `window.__devglideSnifferConfig=${JSON.stringify({
204
+ serverOrigin: `http://localhost:${PORT}`,
205
+ targetPath: path.join(LOGS_DIR, path.basename(dir) + '-console.log'),
206
+ persistent: true,
207
+ allowedTypes: {},
208
+ })};\n`;
209
+
210
+ script += `window.__devglideRunnerConfig=${JSON.stringify({
211
+ serverOrigin: `http://localhost:${PORT}`,
212
+ target: dir,
213
+ })};\n`;
214
+
215
+ script += snifferSource + '\n' + runnerSource;
216
+ res.type('application/javascript').send(script);
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // API routers
221
+ // ---------------------------------------------------------------------------
222
+
223
+ app.use('/api/dashboard', dashboardRouter);
224
+ app.use('/api/kanban', kanbanRouter);
225
+ app.use('/api/log', logRouter);
226
+ app.use('/api/test', testRouter);
227
+ app.use('/api/shell', rateLimit(100, 60_000), shellRouter);
228
+ app.use('/api/coder', coderRouter);
229
+ app.use('/api/workflow', workflowRouter);
230
+ app.use('/api/voice', rateLimit(30, 60_000), voiceRouter);
231
+ app.use('/api/vocabulary', vocabularyRouter);
232
+ app.use('/api/prompts', promptsRouter);
233
+
234
+
235
+ app.use('/', rateLimit(60, 60_000), shellRouter); // /preview, /proxy
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // MCP endpoints
239
+ // ---------------------------------------------------------------------------
240
+
241
+ mountMcpHttp(app, () => createKanbanMcpServer(), '/mcp/kanban');
242
+ mountMcpHttp(app, createLogMcpServer, '/mcp/log');
243
+ mountMcpHttp(app, createTestMcpServer, '/mcp/test');
244
+ mountMcpHttp(app, createVoiceMcpServer, '/mcp/voice');
245
+ mountMcpHttp(app, createWorkflowMcpServer, '/mcp/workflow');
246
+ mountMcpHttp(app, createVocabularyMcpServer, '/mcp/vocabulary');
247
+ mountMcpHttp(app, createPromptsMcpServer, '/mcp/prompts');
248
+
249
+ mountShellMcp(app, '/mcp/shell');
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Socket.io namespaces
253
+ // ---------------------------------------------------------------------------
254
+
255
+ // Dashboard and shell events use the default namespace for backward
256
+ // compatibility — the iframe-loaded frontends connect with io() which
257
+ // hits the default namespace. The event names don't conflict (dashboard
258
+ // uses project:*, shell uses terminal:*/state:*/browser:*).
259
+ initDashboard(io.of('/'));
260
+ initShell(io.of('/'));
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Service initialization
264
+ // ---------------------------------------------------------------------------
265
+
266
+ async function bootstrap() {
267
+ initLog();
268
+
269
+ // Register the server log session directly (same process — no HTTP needed).
270
+ // The sniffer's initial SESSION_START POST fires before the server is listening,
271
+ // so this ensures the session is always discoverable in the log UI.
272
+ const serverLogPath = path.join(ROOT, 'server.log');
273
+ recordSession({
274
+ type: 'SESSION_START',
275
+ session: 'devglide-server',
276
+ ts: new Date().toISOString(),
277
+ url: 'server://devglide',
278
+ ua: `node/${process.version}`,
279
+ persistent: true,
280
+ targetPath: serverLogPath,
281
+ });
282
+
283
+ await initTest();
284
+ initWorkflow();
285
+
286
+ // Restore active project from persistent store
287
+ const stored = getStoredProject();
288
+ if (stored) setActiveProject(stored);
289
+
290
+ // Start listening
291
+ httpServer.listen(PORT, () => {
292
+ console.log(`[devglide] unified server listening on http://localhost:${PORT}`);
293
+ });
294
+ }
295
+
296
+ bootstrap().catch((err) => {
297
+ console.error('[devglide] bootstrap failed:', err);
298
+ process.exit(1);
299
+ });
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Graceful shutdown
303
+ // ---------------------------------------------------------------------------
304
+
305
+ function shutdown() {
306
+ console.log('[devglide] shutting down...');
307
+ shutdownLog();
308
+ shutdownTest();
309
+ shutdownWorkflow();
310
+ shutdownShell();
311
+ io.close();
312
+ httpServer.close(() => {
313
+ console.log('[devglide] server closed');
314
+ process.exit(0);
315
+ });
316
+
317
+ // Force exit after 5 s if open connections prevent graceful shutdown
318
+ setTimeout(() => {
319
+ console.warn('[devglide] forced exit — open connections did not close in time');
320
+ process.exit(1);
321
+ }, 5000).unref();
322
+ }
323
+
324
+ process.on('SIGINT', shutdown);
325
+ process.on('SIGTERM', shutdown);