@exaudeus/workrail 3.40.0 → 3.41.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.
Files changed (75) hide show
  1. package/dist/cli/commands/init.js +0 -3
  2. package/dist/cli-worktrain.js +8 -0
  3. package/dist/cli.js +0 -18
  4. package/dist/config/app-config.d.ts +0 -16
  5. package/dist/config/app-config.js +0 -14
  6. package/dist/config/config-file.js +0 -3
  7. package/dist/console-ui/assets/index-CQt4UhPB.js +28 -0
  8. package/dist/console-ui/assets/index-DGj8EsFR.css +1 -0
  9. package/dist/console-ui/index.html +2 -2
  10. package/dist/coordinators/pr-review.d.ts +17 -0
  11. package/dist/coordinators/pr-review.js +164 -0
  12. package/dist/daemon/daemon-events.d.ts +9 -1
  13. package/dist/daemon/soul-template.d.ts +2 -2
  14. package/dist/daemon/soul-template.js +11 -1
  15. package/dist/daemon/workflow-runner.d.ts +14 -1
  16. package/dist/daemon/workflow-runner.js +395 -25
  17. package/dist/di/container.js +1 -25
  18. package/dist/di/tokens.d.ts +0 -3
  19. package/dist/di/tokens.js +0 -3
  20. package/dist/engine/engine-factory.js +0 -1
  21. package/dist/infrastructure/console-defaults.d.ts +1 -0
  22. package/dist/infrastructure/console-defaults.js +4 -0
  23. package/dist/infrastructure/session/index.d.ts +0 -1
  24. package/dist/infrastructure/session/index.js +1 -3
  25. package/dist/manifest.json +87 -103
  26. package/dist/mcp/handlers/session.d.ts +1 -0
  27. package/dist/mcp/handlers/session.js +61 -13
  28. package/dist/mcp/server.js +1 -18
  29. package/dist/mcp/transports/http-entry.js +0 -2
  30. package/dist/mcp/transports/stdio-entry.js +1 -2
  31. package/dist/mcp/types.d.ts +0 -2
  32. package/dist/trigger/daemon-console.d.ts +2 -0
  33. package/dist/trigger/daemon-console.js +1 -1
  34. package/dist/trigger/trigger-listener.d.ts +2 -0
  35. package/dist/trigger/trigger-listener.js +3 -1
  36. package/dist/trigger/trigger-router.d.ts +4 -3
  37. package/dist/trigger/trigger-router.js +4 -3
  38. package/dist/trigger/trigger-store.js +17 -4
  39. package/dist/v2/usecases/console-routes.d.ts +2 -1
  40. package/dist/v2/usecases/console-routes.js +29 -5
  41. package/dist/v2/usecases/console-service.js +14 -0
  42. package/dist/v2/usecases/console-types.d.ts +1 -0
  43. package/docs/authoring.md +16 -16
  44. package/docs/design/coordinator-message-queue-drain-plan.md +241 -0
  45. package/docs/design/coordinator-message-queue-drain-review.md +120 -0
  46. package/docs/design/coordinator-message-queue-drain.md +289 -0
  47. package/docs/design/shaping-workflow-external-research.md +119 -0
  48. package/docs/discovery/late-bound-goals-impl-plan.md +147 -0
  49. package/docs/discovery/late-bound-goals-review.md +82 -0
  50. package/docs/discovery/late-bound-goals.md +118 -0
  51. package/docs/discovery/steer-endpoint-design-candidates.md +288 -0
  52. package/docs/discovery/steer-endpoint-design-review-findings.md +104 -0
  53. package/docs/discovery/steer-endpoint-implementation-plan.md +284 -0
  54. package/docs/ideas/backlog.md +292 -0
  55. package/docs/ideas/design-candidates-console-session-tree-impl.md +64 -0
  56. package/docs/ideas/design-candidates-session-tree-view.md +196 -0
  57. package/docs/ideas/design-review-findings-console-session-tree-impl.md +75 -0
  58. package/docs/ideas/design-review-findings-session-tree-view.md +88 -0
  59. package/docs/ideas/implementation_plan_session_tree_view.md +238 -0
  60. package/package.json +2 -1
  61. package/spec/authoring-spec.json +16 -16
  62. package/spec/shape.schema.json +178 -0
  63. package/spec/workflow-tags.json +232 -47
  64. package/workflows/coding-task-workflow-agentic.json +491 -480
  65. package/workflows/wr.shaping.json +182 -0
  66. package/dist/console-ui/assets/index-8dh0Psu-.css +0 -1
  67. package/dist/console-ui/assets/index-CXWCAonr.js +0 -28
  68. package/dist/infrastructure/session/DashboardHeartbeat.d.ts +0 -8
  69. package/dist/infrastructure/session/DashboardHeartbeat.js +0 -39
  70. package/dist/infrastructure/session/DashboardLockRelease.d.ts +0 -2
  71. package/dist/infrastructure/session/DashboardLockRelease.js +0 -29
  72. package/dist/infrastructure/session/HttpServer.d.ts +0 -60
  73. package/dist/infrastructure/session/HttpServer.js +0 -912
  74. package/workflows/coding-task-workflow-agentic.lean.v2.json +0 -648
  75. package/workflows/coding-task-workflow-agentic.v2.json +0 -324
@@ -1,912 +0,0 @@
1
- "use strict";
2
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- };
8
- var __metadata = (this && this.__metadata) || function (k, v) {
9
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
- };
11
- var __param = (this && this.__param) || function (paramIndex, decorator) {
12
- return function (target, key) { decorator(target, key, paramIndex); }
13
- };
14
- var __importDefault = (this && this.__importDefault) || function (mod) {
15
- return (mod && mod.__esModule) ? mod : { "default": mod };
16
- };
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.HttpServer = void 0;
19
- const express_1 = __importDefault(require("express"));
20
- const http_1 = require("http");
21
- const path_1 = __importDefault(require("path"));
22
- const promises_1 = __importDefault(require("fs/promises"));
23
- const os_1 = __importDefault(require("os"));
24
- const tsyringe_1 = require("tsyringe");
25
- const tokens_js_1 = require("../../di/tokens.js");
26
- const SessionManager_js_1 = require("./SessionManager.js");
27
- const DashboardHeartbeat_js_1 = require("./DashboardHeartbeat.js");
28
- const DashboardLockRelease_js_1 = require("./DashboardLockRelease.js");
29
- const cors_1 = __importDefault(require("cors"));
30
- const open_1 = __importDefault(require("open"));
31
- const child_process_1 = require("child_process");
32
- const _pkg = require(path_1.default.join(__dirname, '../../../package.json'));
33
- const CURRENT_VERSION = _pkg.version;
34
- let HttpServer = class HttpServer {
35
- constructor(sessionManager, processLifecyclePolicy, processSignals, shutdownEvents, dashboardMode, browserBehavior) {
36
- this.sessionManager = sessionManager;
37
- this.processLifecyclePolicy = processLifecyclePolicy;
38
- this.processSignals = processSignals;
39
- this.shutdownEvents = shutdownEvents;
40
- this.dashboardMode = dashboardMode;
41
- this.browserBehavior = browserBehavior;
42
- this.server = null;
43
- this.baseUrl = '';
44
- this.isPrimary = false;
45
- this._stopPromise = null;
46
- this.config = {};
47
- this._routeDisposers = [];
48
- this.port = 3456;
49
- this.lockFile = path_1.default.join(os_1.default.homedir(), '.workrail', 'dashboard.lock');
50
- this.heartbeat = new DashboardHeartbeat_js_1.DashboardHeartbeat(this.lockFile, () => this.isPrimary);
51
- this.app = (0, express_1.default)();
52
- this.setupMiddleware();
53
- this.setupRoutes();
54
- }
55
- setConfig(config) {
56
- this.config = config;
57
- if (config.port) {
58
- this.port = config.port;
59
- }
60
- if (config.lockFilePath) {
61
- this.lockFile = config.lockFilePath;
62
- this.heartbeat = new DashboardHeartbeat_js_1.DashboardHeartbeat(this.lockFile, () => this.isPrimary);
63
- }
64
- return this;
65
- }
66
- setupMiddleware() {
67
- this.app.use((0, cors_1.default)({
68
- origin: '*',
69
- methods: ['GET', 'HEAD', 'OPTIONS'],
70
- allowedHeaders: ['Content-Type', 'If-None-Match']
71
- }));
72
- this.app.set('etag', 'strong');
73
- this.app.use(express_1.default.json());
74
- this.app.use((req, res, next) => {
75
- const start = Date.now();
76
- res.on('finish', () => {
77
- const duration = Date.now() - start;
78
- try {
79
- process.stderr.write(`[HTTP] ${req.method} ${req.path} ${res.statusCode} (${duration}ms)\n`);
80
- }
81
- catch { }
82
- });
83
- next();
84
- });
85
- }
86
- setupRoutes() {
87
- this.app.get('/', (_req, res) => {
88
- res.redirect('/console');
89
- });
90
- this.app.get('/api/sessions', async (req, res) => {
91
- try {
92
- const sessions = this.isPrimary
93
- ? await this.sessionManager.listAllProjectsSessions()
94
- : await this.sessionManager.listAllSessions();
95
- res.json({
96
- success: true,
97
- count: sessions.length,
98
- unified: this.isPrimary,
99
- sessions: sessions.map(s => ({
100
- id: s.id,
101
- workflowId: s.workflowId,
102
- projectId: s.projectId,
103
- projectPath: s.projectPath,
104
- createdAt: s.createdAt,
105
- updatedAt: s.updatedAt,
106
- url: `/api/sessions/${s.workflowId}/${s.id}`,
107
- data: {
108
- dashboard: s.data?.dashboard || {}
109
- }
110
- }))
111
- });
112
- }
113
- catch (error) {
114
- res.status(500).json({
115
- success: false,
116
- error: 'Failed to list sessions',
117
- message: error.message
118
- });
119
- }
120
- });
121
- this.app.get('/api/sessions/:workflow/:id', async (req, res) => {
122
- try {
123
- const { workflow, id } = req.params;
124
- const session = await this.sessionManager.getSession(workflow, id);
125
- if (!session) {
126
- return res.status(404).json({
127
- success: false,
128
- error: 'Session not found',
129
- workflowId: workflow,
130
- sessionId: id
131
- });
132
- }
133
- res.json({
134
- success: true,
135
- session
136
- });
137
- }
138
- catch (error) {
139
- res.status(500).json({
140
- success: false,
141
- error: 'Failed to get session',
142
- message: error.message
143
- });
144
- }
145
- });
146
- this.app.get('/api/current-project', async (req, res) => {
147
- try {
148
- const project = await this.sessionManager.getCurrentProject();
149
- res.json({
150
- success: true,
151
- project
152
- });
153
- }
154
- catch (error) {
155
- res.status(500).json({
156
- success: false,
157
- error: 'Failed to get project info',
158
- message: error.message
159
- });
160
- }
161
- });
162
- this.app.get('/api/projects', async (req, res) => {
163
- try {
164
- const projects = await this.sessionManager.listProjects();
165
- res.json({
166
- success: true,
167
- count: projects.length,
168
- projects
169
- });
170
- }
171
- catch (error) {
172
- res.status(500).json({
173
- success: false,
174
- error: 'Failed to list projects',
175
- message: error.message
176
- });
177
- }
178
- });
179
- this.app.get('/api/sessions/:workflow/:id/stream', async (req, res) => {
180
- const { workflow, id } = req.params;
181
- let isCleanedUp = false;
182
- let keepaliveInterval = null;
183
- let maxConnectionTimeout = null;
184
- const cleanup = () => {
185
- if (isCleanedUp)
186
- return;
187
- isCleanedUp = true;
188
- if (keepaliveInterval) {
189
- clearInterval(keepaliveInterval);
190
- keepaliveInterval = null;
191
- }
192
- if (maxConnectionTimeout) {
193
- clearTimeout(maxConnectionTimeout);
194
- maxConnectionTimeout = null;
195
- }
196
- try {
197
- this.sessionManager.off('session:updated', onUpdate);
198
- }
199
- catch { }
200
- try {
201
- this.sessionManager.unwatchSession(workflow, id);
202
- }
203
- catch { }
204
- try {
205
- if (!res.writableEnded) {
206
- res.end();
207
- }
208
- }
209
- catch { }
210
- };
211
- const onUpdate = (event) => {
212
- if (isCleanedUp || event.workflowId !== workflow || event.sessionId !== id) {
213
- return;
214
- }
215
- if (res.writableEnded) {
216
- cleanup();
217
- return;
218
- }
219
- try {
220
- res.write(`data: ${JSON.stringify({ type: 'update', session: event.session })}\n\n`);
221
- }
222
- catch (error) {
223
- try {
224
- process.stderr.write(`[SSE] Write error for ${workflow}/${id}: ${error?.message ?? String(error)}\n`);
225
- }
226
- catch { }
227
- cleanup();
228
- }
229
- };
230
- const safeWrite = (data) => {
231
- if (isCleanedUp || res.writableEnded) {
232
- cleanup();
233
- return false;
234
- }
235
- try {
236
- res.write(data);
237
- return true;
238
- }
239
- catch (error) {
240
- try {
241
- process.stderr.write(`[SSE] Write error for ${workflow}/${id}: ${error?.message ?? String(error)}\n`);
242
- }
243
- catch { }
244
- cleanup();
245
- return false;
246
- }
247
- };
248
- res.setHeader('Content-Type', 'text/event-stream');
249
- res.setHeader('Cache-Control', 'no-cache');
250
- res.setHeader('Connection', 'keep-alive');
251
- res.setHeader('X-Accel-Buffering', 'no');
252
- maxConnectionTimeout = setTimeout(() => {
253
- try {
254
- process.stderr.write(`[SSE] Max connection time reached for ${workflow}/${id}, closing\n`);
255
- }
256
- catch { }
257
- cleanup();
258
- }, 30 * 60 * 1000);
259
- if (!safeWrite(`data: ${JSON.stringify({ type: 'connected', workflowId: workflow, sessionId: id })}\n\n`)) {
260
- return;
261
- }
262
- try {
263
- const session = await this.sessionManager.getSession(workflow, id);
264
- if (session && !isCleanedUp) {
265
- safeWrite(`data: ${JSON.stringify({ type: 'update', session })}\n\n`);
266
- }
267
- }
268
- catch (error) {
269
- }
270
- this.sessionManager.on('session:updated', onUpdate);
271
- this.sessionManager.watchSession(workflow, id);
272
- keepaliveInterval = setInterval(() => {
273
- if (!safeWrite(`:keepalive\n\n`)) {
274
- }
275
- }, 30000);
276
- req.on('close', cleanup);
277
- req.on('error', (error) => {
278
- try {
279
- process.stderr.write(`[SSE] Request error for ${workflow}/${id}: ${error?.message ?? String(error)}\n`);
280
- }
281
- catch { }
282
- cleanup();
283
- });
284
- res.on('error', (error) => {
285
- try {
286
- process.stderr.write(`[SSE] Response error for ${workflow}/${id}: ${error?.message ?? String(error)}\n`);
287
- }
288
- catch { }
289
- cleanup();
290
- });
291
- res.on('finish', cleanup);
292
- });
293
- this.app.delete('/api/sessions/:workflow/:id', async (req, res) => {
294
- try {
295
- const { workflow, id } = req.params;
296
- const result = await this.sessionManager.deleteSession(workflow, id);
297
- if (result.isErr()) {
298
- const status = result.error.code === 'SESSION_NOT_FOUND' ? 404 : 500;
299
- res.status(status).json({ success: false, error: result.error.message });
300
- return;
301
- }
302
- res.json({
303
- success: true,
304
- message: `Session ${workflow}/${id} deleted successfully`
305
- });
306
- }
307
- catch (error) {
308
- try {
309
- process.stderr.write(`[HttpServer] Delete session error: ${error?.message ?? String(error)}\n`);
310
- }
311
- catch { }
312
- res.status(500).json({
313
- success: false,
314
- error: error.message || 'Failed to delete session'
315
- });
316
- }
317
- });
318
- this.app.post('/api/sessions/bulk-delete', async (req, res) => {
319
- try {
320
- const { sessions } = req.body;
321
- if (!Array.isArray(sessions)) {
322
- return res.status(400).json({
323
- success: false,
324
- error: 'Body must contain "sessions" array'
325
- });
326
- }
327
- await this.sessionManager.deleteSessions(sessions);
328
- res.json({
329
- success: true,
330
- message: `Deleted ${sessions.length} session(s)`,
331
- count: sessions.length
332
- });
333
- }
334
- catch (error) {
335
- try {
336
- process.stderr.write(`[HttpServer] Bulk delete error: ${error?.message ?? String(error)}\n`);
337
- }
338
- catch { }
339
- res.status(500).json({
340
- success: false,
341
- error: error.message || 'Failed to delete sessions'
342
- });
343
- }
344
- });
345
- this.app.get('/api/health', (_req, res) => {
346
- res.json({
347
- success: true,
348
- status: 'healthy',
349
- uptime: process.uptime(),
350
- timestamp: new Date().toISOString(),
351
- isPrimary: this.isPrimary,
352
- pid: process.pid,
353
- port: this.port,
354
- version: CURRENT_VERSION
355
- });
356
- });
357
- }
358
- async start() {
359
- this._stopPromise = null;
360
- const mode = this.config.dashboardMode ?? this.dashboardMode;
361
- if (mode.kind === 'legacy') {
362
- try {
363
- process.stderr.write('[Dashboard] Unified dashboard disabled, using legacy mode\n');
364
- }
365
- catch { }
366
- return await this.startLegacyMode();
367
- }
368
- if (await this.tryBecomePrimary()) {
369
- try {
370
- await this.startAsPrimary();
371
- return this.baseUrl;
372
- }
373
- catch (error) {
374
- if (error.code === 'EADDRINUSE') {
375
- try {
376
- process.stderr.write(`[Dashboard] Port ${this.port} still held by previous instance -- ` +
377
- `running on next available port. Restart the old instance to move to ${this.port}.\n`);
378
- }
379
- catch { }
380
- await promises_1.default.unlink(this.lockFile).catch(() => { });
381
- return await this.startLegacyMode();
382
- }
383
- throw error;
384
- }
385
- }
386
- else {
387
- try {
388
- process.stderr.write(`[Dashboard] Unified dashboard at http://localhost:${this.port}\n`);
389
- }
390
- catch { }
391
- return null;
392
- }
393
- }
394
- async tryBecomePrimary() {
395
- try {
396
- await promises_1.default.mkdir(path_1.default.dirname(this.lockFile), { recursive: true });
397
- const lockData = {
398
- pid: process.pid,
399
- port: this.port,
400
- startedAt: new Date().toISOString(),
401
- lastHeartbeat: new Date().toISOString(),
402
- projectId: this.sessionManager.getProjectId(),
403
- projectPath: this.sessionManager.getProjectPath(),
404
- version: CURRENT_VERSION
405
- };
406
- await promises_1.default.writeFile(this.lockFile, JSON.stringify(lockData, null, 2), { flag: 'wx' });
407
- try {
408
- process.stderr.write('[Dashboard] Primary elected\n');
409
- }
410
- catch { }
411
- this.isPrimary = true;
412
- this.setupPrimaryCleanup();
413
- this.heartbeat.start();
414
- return true;
415
- }
416
- catch (error) {
417
- if (error.code === 'EEXIST') {
418
- return await this.reclaimStaleLock();
419
- }
420
- else if (error.code === 'EACCES' || error.code === 'EPERM') {
421
- try {
422
- process.stderr.write('[Dashboard] Cannot write lock file (permission denied)\n');
423
- }
424
- catch { }
425
- return false;
426
- }
427
- throw error;
428
- }
429
- }
430
- shouldReclaimLock(lockData) {
431
- if (!lockData.pid || !lockData.port || !lockData.startedAt) {
432
- return { reclaim: true, reason: 'invalid lock structure' };
433
- }
434
- if (lockData.projectId) {
435
- const currentProjectId = this.sessionManager.getProjectId();
436
- if (lockData.projectId !== currentProjectId) {
437
- try {
438
- process.kill(lockData.pid, 0);
439
- return { reclaim: false, reason: `different project, primary alive (lock=${lockData.projectId}, current=${currentProjectId})` };
440
- }
441
- catch {
442
- }
443
- }
444
- }
445
- if (lockData.version !== CURRENT_VERSION) {
446
- return { reclaim: true, reason: `version mismatch (lock=${lockData.version}, current=${CURRENT_VERSION})` };
447
- }
448
- const lastHeartbeat = new Date(lockData.lastHeartbeat || lockData.startedAt);
449
- const ageMinutes = (Date.now() - lastHeartbeat.getTime()) / 60000;
450
- if (ageMinutes > 2) {
451
- return { reclaim: true, reason: `stale (${ageMinutes.toFixed(1)}min old)` };
452
- }
453
- try {
454
- process.kill(lockData.pid, 0);
455
- }
456
- catch {
457
- return { reclaim: true, reason: `PID ${lockData.pid} dead` };
458
- }
459
- return { reclaim: false, reason: 'valid' };
460
- }
461
- async reclaimStaleLock() {
462
- try {
463
- const lockContent = await promises_1.default.readFile(this.lockFile, 'utf-8');
464
- const lockData = JSON.parse(lockContent);
465
- const { reclaim, reason } = this.shouldReclaimLock(lockData);
466
- if (!reclaim) {
467
- try {
468
- process.stderr.write(`[Dashboard] Secondary mode: primary lock valid (PID ${lockData.pid}), yielding\n`);
469
- }
470
- catch { }
471
- return false;
472
- }
473
- else {
474
- try {
475
- process.stderr.write(`[Dashboard] Lock reclaim needed: ${reason}\n`);
476
- }
477
- catch { }
478
- }
479
- const tempPath = `${this.lockFile}.${process.pid}.${Date.now()}`;
480
- const newLockData = {
481
- pid: process.pid,
482
- port: this.port,
483
- startedAt: new Date().toISOString(),
484
- lastHeartbeat: new Date().toISOString(),
485
- projectId: this.sessionManager.getProjectId(),
486
- projectPath: this.sessionManager.getProjectPath(),
487
- version: CURRENT_VERSION
488
- };
489
- try {
490
- await promises_1.default.writeFile(tempPath, JSON.stringify(newLockData, null, 2));
491
- let retries = 3;
492
- let renamed = false;
493
- while (retries > 0 && !renamed) {
494
- try {
495
- await promises_1.default.rename(tempPath, this.lockFile);
496
- renamed = true;
497
- }
498
- catch (err) {
499
- if (err.code === 'EPERM' && process.platform === 'win32' && retries > 1) {
500
- await new Promise(resolve => setTimeout(resolve, 10));
501
- retries--;
502
- continue;
503
- }
504
- throw err;
505
- }
506
- }
507
- try {
508
- const writtenContent = await promises_1.default.readFile(this.lockFile, 'utf-8');
509
- const writtenData = JSON.parse(writtenContent);
510
- if (writtenData.pid !== process.pid) {
511
- try {
512
- process.stderr.write(`[Dashboard] Lost lock election (winner PID ${writtenData.pid}), yielding\n`);
513
- }
514
- catch { }
515
- return false;
516
- }
517
- }
518
- catch {
519
- return await this.tryBecomePrimary();
520
- }
521
- try {
522
- process.stderr.write('[Dashboard] Lock reclaimed successfully\n');
523
- }
524
- catch { }
525
- this.isPrimary = true;
526
- this.setupPrimaryCleanup();
527
- this.heartbeat.start();
528
- return true;
529
- }
530
- catch (error) {
531
- await promises_1.default.unlink(tempPath).catch(() => { });
532
- if (error.code === 'ENOENT') {
533
- try {
534
- process.stderr.write('[Dashboard] Lock deleted during reclaim, trying fresh\n');
535
- }
536
- catch { }
537
- return await this.tryBecomePrimary();
538
- }
539
- try {
540
- process.stderr.write(`[Dashboard] Lock reclaim failed: ${error.message}\n`);
541
- }
542
- catch { }
543
- return false;
544
- }
545
- }
546
- catch (error) {
547
- if (error.code === 'ENOENT') {
548
- return await this.tryBecomePrimary();
549
- }
550
- try {
551
- process.stderr.write('[Dashboard] Lock file corrupted, attempting fresh claim\n');
552
- }
553
- catch { }
554
- await promises_1.default.unlink(this.lockFile).catch(() => { });
555
- return await this.tryBecomePrimary();
556
- }
557
- }
558
- setupPrimaryCleanup() {
559
- if (this.processLifecyclePolicy.kind === 'no_signal_handlers') {
560
- return;
561
- }
562
- let isCleaningUp = false;
563
- const cleanupSync = () => {
564
- if (isCleaningUp || !this.isPrimary)
565
- return;
566
- isCleaningUp = true;
567
- try {
568
- process.stderr.write('[Dashboard] Primary shutting down (sync cleanup)\n');
569
- }
570
- catch { }
571
- this.heartbeat.stop();
572
- try {
573
- (0, DashboardLockRelease_js_1.releaseLockFileSync)(this.lockFile);
574
- try {
575
- process.stderr.write('[Dashboard] Lock file released\n');
576
- }
577
- catch { }
578
- }
579
- catch (error) {
580
- if (error.code !== 'ENOENT') {
581
- try {
582
- process.stderr.write(`[Dashboard] Failed to release lock file: ${error.message}\n`);
583
- }
584
- catch { }
585
- }
586
- }
587
- this.isPrimary = false;
588
- };
589
- const signalHandler = (signal) => {
590
- if (isCleaningUp)
591
- return;
592
- isCleaningUp = true;
593
- try {
594
- process.stderr.write(`[Dashboard] Received ${signal}\n`);
595
- }
596
- catch { }
597
- this.stop()
598
- .catch(err => { try {
599
- process.stderr.write(`[Dashboard] Cleanup error: ${err.message}\n`);
600
- }
601
- catch { } })
602
- .finally(() => {
603
- if (signal !== 'exit') {
604
- this.shutdownEvents.emit({ kind: 'shutdown_requested', signal });
605
- }
606
- });
607
- };
608
- this.processSignals.on('exit', cleanupSync);
609
- this.processSignals.on('SIGINT', () => signalHandler('SIGINT'));
610
- this.processSignals.on('SIGTERM', () => signalHandler('SIGTERM'));
611
- this.processSignals.on('SIGHUP', () => signalHandler('SIGHUP'));
612
- }
613
- async startAsPrimary() {
614
- await new Promise((resolve, reject) => {
615
- this.server = (0, http_1.createServer)(this.app);
616
- this.server.on('error', (error) => {
617
- reject(error);
618
- });
619
- const listenPort = this.port;
620
- this.server.listen(listenPort, '127.0.0.1', () => {
621
- this.baseUrl = `http://localhost:${listenPort}`;
622
- this.printBanner();
623
- resolve();
624
- });
625
- });
626
- }
627
- async startLegacyMode() {
628
- this.port = 3457;
629
- while (this.port < 3500) {
630
- try {
631
- await new Promise((resolve, reject) => {
632
- this.server = (0, http_1.createServer)(this.app);
633
- this.server.on('error', (error) => {
634
- if (error.code === 'EADDRINUSE') {
635
- reject(new Error('Port in use'));
636
- }
637
- else {
638
- reject(error);
639
- }
640
- });
641
- this.server.listen(this.port, '127.0.0.1', () => {
642
- resolve();
643
- });
644
- });
645
- this.baseUrl = `http://localhost:${this.port}`;
646
- try {
647
- process.stderr.write(`[Dashboard] Started in legacy mode on port ${this.port}\n`);
648
- }
649
- catch { }
650
- this.printBanner();
651
- return this.baseUrl;
652
- }
653
- catch (error) {
654
- if (error.message === 'Port in use') {
655
- this.port++;
656
- continue;
657
- }
658
- throw error;
659
- }
660
- }
661
- this.server = null;
662
- throw new Error('No available ports in range 3457-3499');
663
- }
664
- printBanner() {
665
- const line = '═'.repeat(60);
666
- try {
667
- process.stderr.write(`\n${line}\n`);
668
- }
669
- catch { }
670
- try {
671
- process.stderr.write(`🔧 Workrail MCP Server Started\n`);
672
- }
673
- catch { }
674
- try {
675
- process.stderr.write(`${line}\n`);
676
- }
677
- catch { }
678
- try {
679
- process.stderr.write(`📊 Dashboard: ${this.baseUrl} ${this.isPrimary ? '(PRIMARY - All Projects)' : '(Legacy Mode)'}\n`);
680
- }
681
- catch { }
682
- try {
683
- process.stderr.write(`💾 Sessions: ${this.sessionManager.getSessionsRoot()}\n`);
684
- }
685
- catch { }
686
- try {
687
- process.stderr.write(`🏗️ Project: ${this.sessionManager.getProjectId()}\n`);
688
- }
689
- catch { }
690
- try {
691
- process.stderr.write(`${line}\n\n`);
692
- }
693
- catch { }
694
- }
695
- async openDashboard(sessionId) {
696
- if (!this.baseUrl) {
697
- throw new Error('Dashboard is unavailable -- the HTTP server did not start successfully ' +
698
- '(likely due to port exhaustion). MCP tools still work normally.');
699
- }
700
- let url = this.baseUrl;
701
- if (sessionId) {
702
- url += `?session=${sessionId}`;
703
- }
704
- const behavior = this.config.browserBehavior ?? this.browserBehavior;
705
- if (behavior.kind === 'auto_open') {
706
- try {
707
- await (0, open_1.default)(url);
708
- try {
709
- process.stderr.write(`Opened dashboard: ${url}\n`);
710
- }
711
- catch { }
712
- }
713
- catch (error) {
714
- try {
715
- process.stderr.write(`Dashboard URL: ${url} (auto-open failed, please open manually)\n`);
716
- }
717
- catch { }
718
- }
719
- }
720
- return url;
721
- }
722
- async stop() {
723
- if (this._stopPromise !== null)
724
- return this._stopPromise;
725
- if (this.server === null)
726
- return Promise.resolve();
727
- this._stopPromise = this._runStop();
728
- return this._stopPromise;
729
- }
730
- async _runStop() {
731
- this.heartbeat.stop();
732
- this.sessionManager.unwatchAll();
733
- for (const dispose of this._routeDisposers) {
734
- try {
735
- dispose();
736
- }
737
- catch { }
738
- }
739
- this._routeDisposers.length = 0;
740
- await new Promise((resolve) => {
741
- if (!this.server)
742
- return resolve();
743
- const closeTimeout = setTimeout(() => {
744
- try {
745
- process.stderr.write('[Dashboard] Server close timeout after 5s, forcing shutdown\n');
746
- }
747
- catch { }
748
- resolve();
749
- }, 5000);
750
- this.server.close(() => {
751
- clearTimeout(closeTimeout);
752
- try {
753
- process.stderr.write('HTTP server stopped\n');
754
- }
755
- catch { }
756
- resolve();
757
- });
758
- });
759
- if (this.isPrimary) {
760
- await (0, DashboardLockRelease_js_1.releaseLockFile)(this.lockFile).catch(() => { });
761
- this.isPrimary = false;
762
- }
763
- }
764
- mountRoutes(installer) {
765
- const disposer = installer(this.app);
766
- if (disposer != null) {
767
- this._routeDisposers.push(disposer);
768
- }
769
- }
770
- finalize() {
771
- this.app.use((req, res) => {
772
- res.status(404).json({
773
- success: false,
774
- error: 'Not found',
775
- path: req.path,
776
- });
777
- });
778
- }
779
- getBaseUrl() {
780
- return this.baseUrl;
781
- }
782
- getPort() {
783
- return this.port;
784
- }
785
- async fullCleanup() {
786
- try {
787
- const busyPorts = await this.getWorkrailPorts();
788
- if (busyPorts.length === 0) {
789
- try {
790
- process.stderr.write('[Cleanup] No workrail processes found\n');
791
- }
792
- catch { }
793
- return 0;
794
- }
795
- try {
796
- process.stderr.write(`[Cleanup] Found ${busyPorts.length} workrail process(es), removing all...\n`);
797
- }
798
- catch { }
799
- let cleanedCount = 0;
800
- for (const { port, pid } of busyPorts) {
801
- if (pid === process.pid) {
802
- try {
803
- process.stderr.write(`[Cleanup] Skipping current process ${pid}\n`);
804
- }
805
- catch { }
806
- continue;
807
- }
808
- try {
809
- process.stderr.write(`[Cleanup] Killing process ${pid} on port ${port}\n`);
810
- }
811
- catch { }
812
- try {
813
- process.kill(pid, 'SIGTERM');
814
- await new Promise(r => setTimeout(r, 1000));
815
- try {
816
- process.kill(pid, 0);
817
- process.kill(pid, 'SIGKILL');
818
- try {
819
- process.stderr.write(`[Cleanup] Force killed process ${pid}\n`);
820
- }
821
- catch { }
822
- }
823
- catch {
824
- try {
825
- process.stderr.write(`[Cleanup] Process ${pid} terminated gracefully\n`);
826
- }
827
- catch { }
828
- }
829
- cleanedCount++;
830
- }
831
- catch (error) {
832
- try {
833
- process.stderr.write(`[Cleanup] Failed to kill process ${pid}: ${error?.message ?? String(error)}\n`);
834
- }
835
- catch { }
836
- }
837
- }
838
- try {
839
- process.stderr.write(`[Cleanup] Cleaned up ${cleanedCount} process(es)\n`);
840
- }
841
- catch { }
842
- try {
843
- await promises_1.default.unlink(this.lockFile);
844
- try {
845
- process.stderr.write('[Cleanup] Removed lock file\n');
846
- }
847
- catch { }
848
- }
849
- catch {
850
- }
851
- return cleanedCount;
852
- }
853
- catch (error) {
854
- try {
855
- process.stderr.write(`[Cleanup] Full cleanup failed: ${error?.message ?? String(error)}\n`);
856
- }
857
- catch { }
858
- throw error;
859
- }
860
- }
861
- async getWorkrailPorts() {
862
- try {
863
- const platform = os_1.default.platform();
864
- if (platform === 'darwin' || platform === 'linux') {
865
- const output = (0, child_process_1.execSync)('lsof -i :3456-3499 -Pn 2>/dev/null | grep node || true', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
866
- if (!output)
867
- return [];
868
- return output.split('\n').filter(Boolean).map(line => {
869
- const parts = line.trim().split(/\s+/);
870
- const pid = parseInt(parts[1]);
871
- const nameField = parts[8];
872
- const portMatch = nameField.match(/:(\d+)$/);
873
- const port = portMatch ? parseInt(portMatch[1]) : 0;
874
- return { port, pid };
875
- }).filter(item => item.port >= 3456 && item.port < 3500 && !isNaN(item.pid));
876
- }
877
- else if (platform === 'win32') {
878
- const output = (0, child_process_1.execSync)('netstat -ano | findstr "3456" || echo', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
879
- if (!output)
880
- return [];
881
- return output.split('\n').filter(Boolean).map(line => {
882
- const parts = line.trim().split(/\s+/);
883
- const state = parts[3] || '';
884
- const address = parts[1] || '';
885
- const pid = parseInt(parts[4]);
886
- const portMatch = address.match(/:(\d+)$/);
887
- const port = portMatch ? parseInt(portMatch[1]) : 0;
888
- return { port, pid, state };
889
- }).filter(item => item.state === 'LISTENING' &&
890
- item.port >= 3456 &&
891
- item.port < 3500 &&
892
- !isNaN(item.pid) &&
893
- item.pid > 0).map(({ port, pid }) => ({ port, pid }));
894
- }
895
- return [];
896
- }
897
- catch {
898
- return [];
899
- }
900
- }
901
- };
902
- exports.HttpServer = HttpServer;
903
- exports.HttpServer = HttpServer = __decorate([
904
- (0, tsyringe_1.singleton)(),
905
- __param(0, (0, tsyringe_1.inject)(SessionManager_js_1.SessionManager)),
906
- __param(1, (0, tsyringe_1.inject)(tokens_js_1.DI.Runtime.ProcessLifecyclePolicy)),
907
- __param(2, (0, tsyringe_1.inject)(tokens_js_1.DI.Runtime.ProcessSignals)),
908
- __param(3, (0, tsyringe_1.inject)(tokens_js_1.DI.Runtime.ShutdownEvents)),
909
- __param(4, (0, tsyringe_1.inject)(tokens_js_1.DI.Config.DashboardMode)),
910
- __param(5, (0, tsyringe_1.inject)(tokens_js_1.DI.Config.BrowserBehavior)),
911
- __metadata("design:paramtypes", [SessionManager_js_1.SessionManager, Object, Object, Object, Object, Object])
912
- ], HttpServer);