agileflow 3.0.2 → 3.2.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.
- package/CHANGELOG.md +10 -0
- package/README.md +58 -86
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/feedback.js +9 -2
- package/lib/lazy-require.js +59 -0
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +14 -2
- package/scripts/agileflow-welcome.js +450 -459
- package/scripts/claude-tmux.sh +113 -5
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +411 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +90 -0
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/legal-analyzer-a11y.md +110 -0
- package/src/core/agents/legal-analyzer-ai.md +117 -0
- package/src/core/agents/legal-analyzer-consumer.md +108 -0
- package/src/core/agents/legal-analyzer-content.md +113 -0
- package/src/core/agents/legal-analyzer-international.md +115 -0
- package/src/core/agents/legal-analyzer-licensing.md +115 -0
- package/src/core/agents/legal-analyzer-privacy.md +108 -0
- package/src/core/agents/legal-analyzer-security.md +112 -0
- package/src/core/agents/legal-analyzer-terms.md +111 -0
- package/src/core/agents/legal-consensus.md +242 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/team-lead.md +50 -13
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/audit/legal.md +446 -0
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/configure.md +18 -33
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -67
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -75
- package/scripts/tmux-task-watcher.sh +0 -177
package/lib/dashboard-server.js
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dashboard-server.js - WebSocket Server for AgileFlow Dashboard
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
* - Multiple client support
|
|
5
|
+
* Coordinator module that delegates to focused domain modules:
|
|
6
|
+
* - dashboard-websocket.js - WebSocket frame encode/decode
|
|
7
|
+
* - dashboard-session.js - Session lifecycle and rate limiting
|
|
8
|
+
* - dashboard-terminal.js - Terminal management (PTY/fallback)
|
|
9
|
+
* - dashboard-git.js - Git operations (status, diff, actions)
|
|
10
|
+
* - dashboard-automations.js - Automation scheduling
|
|
11
|
+
* - dashboard-status.js - Project status and team metrics
|
|
12
|
+
* - dashboard-inbox.js - Inbox management
|
|
14
13
|
*
|
|
15
14
|
* Usage:
|
|
16
15
|
* const { createDashboardServer, startDashboardServer } = require('./dashboard-server');
|
|
@@ -23,6 +22,28 @@
|
|
|
23
22
|
|
|
24
23
|
const { EventEmitter } = require('events');
|
|
25
24
|
|
|
25
|
+
// Import extracted modules
|
|
26
|
+
const { encodeWebSocketFrame, decodeWebSocketFrame } = require('./dashboard-websocket');
|
|
27
|
+
const {
|
|
28
|
+
DashboardSession,
|
|
29
|
+
SESSION_TIMEOUT_MS,
|
|
30
|
+
SESSION_CLEANUP_INTERVAL_MS,
|
|
31
|
+
RATE_LIMIT_TOKENS,
|
|
32
|
+
} = require('./dashboard-session');
|
|
33
|
+
const {
|
|
34
|
+
TerminalInstance,
|
|
35
|
+
TerminalManager,
|
|
36
|
+
SENSITIVE_ENV_PATTERNS,
|
|
37
|
+
} = require('./dashboard-terminal');
|
|
38
|
+
const { getGitStatus, getFileDiff, parseDiffStats, handleGitAction } = require('./dashboard-git');
|
|
39
|
+
const {
|
|
40
|
+
calculateNextRun,
|
|
41
|
+
createInboxItem,
|
|
42
|
+
enrichAutomationList,
|
|
43
|
+
} = require('./dashboard-automations');
|
|
44
|
+
const { buildStatusSummary, readTeamMetrics } = require('./dashboard-status');
|
|
45
|
+
const { getSortedInboxItems, handleInboxAction } = require('./dashboard-inbox');
|
|
46
|
+
|
|
26
47
|
// Lazy-loaded dependencies - deferred until first use
|
|
27
48
|
let _http, _crypto, _protocol, _paths, _validatePaths, _childProcess;
|
|
28
49
|
|
|
@@ -75,434 +96,9 @@ function getAutomationRunner(rootDir) {
|
|
|
75
96
|
const DEFAULT_PORT = 8765;
|
|
76
97
|
const DEFAULT_HOST = '127.0.0.1'; // Localhost only for security
|
|
77
98
|
|
|
78
|
-
// Session lifecycle
|
|
79
|
-
const SESSION_TIMEOUT_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
80
|
-
const SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
81
|
-
|
|
82
|
-
// Rate limiting (token bucket)
|
|
83
|
-
const RATE_LIMIT_TOKENS = 100; // max messages per second
|
|
84
|
-
const RATE_LIMIT_REFILL_MS = 1000; // refill interval
|
|
85
|
-
|
|
86
|
-
// Sensitive env var patterns to strip from terminal spawn
|
|
87
|
-
const SENSITIVE_ENV_PATTERNS = /SECRET|TOKEN|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY|AUTH/i;
|
|
88
|
-
|
|
89
99
|
// WebSocket magic GUID for handshake
|
|
90
100
|
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
91
101
|
|
|
92
|
-
/**
|
|
93
|
-
* Session state for a connected dashboard
|
|
94
|
-
*/
|
|
95
|
-
class DashboardSession {
|
|
96
|
-
constructor(id, ws, projectRoot) {
|
|
97
|
-
this.id = id;
|
|
98
|
-
this.ws = ws;
|
|
99
|
-
this.projectRoot = projectRoot;
|
|
100
|
-
this.messages = [];
|
|
101
|
-
this.state = 'connected';
|
|
102
|
-
this.lastActivity = new Date();
|
|
103
|
-
this.createdAt = new Date();
|
|
104
|
-
this.metadata = {};
|
|
105
|
-
|
|
106
|
-
// Token bucket rate limiter
|
|
107
|
-
this._rateTokens = RATE_LIMIT_TOKENS;
|
|
108
|
-
this._rateLastRefill = Date.now();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Check if session has expired
|
|
113
|
-
* @returns {boolean}
|
|
114
|
-
*/
|
|
115
|
-
isExpired() {
|
|
116
|
-
return Date.now() - this.lastActivity.getTime() > SESSION_TIMEOUT_MS;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Rate-limit incoming messages (token bucket)
|
|
121
|
-
* @returns {boolean} true if allowed, false if rate-limited
|
|
122
|
-
*/
|
|
123
|
-
checkRateLimit() {
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
const elapsed = now - this._rateLastRefill;
|
|
126
|
-
|
|
127
|
-
// Refill tokens based on elapsed time
|
|
128
|
-
if (elapsed >= RATE_LIMIT_REFILL_MS) {
|
|
129
|
-
this._rateTokens = RATE_LIMIT_TOKENS;
|
|
130
|
-
this._rateLastRefill = now;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (this._rateTokens <= 0) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this._rateTokens--;
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Send a message to the dashboard
|
|
143
|
-
* @param {Object} message - Message object
|
|
144
|
-
*/
|
|
145
|
-
send(message) {
|
|
146
|
-
if (this.ws && this.ws.writable) {
|
|
147
|
-
try {
|
|
148
|
-
const frame = encodeWebSocketFrame(getProtocol().serializeMessage(message));
|
|
149
|
-
this.ws.write(frame);
|
|
150
|
-
this.lastActivity = new Date();
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error(`[Session ${this.id}] Send error:`, error.message);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Add a message to conversation history
|
|
159
|
-
* @param {string} role - 'user' or 'assistant'
|
|
160
|
-
* @param {string} content - Message content
|
|
161
|
-
*/
|
|
162
|
-
addMessage(role, content) {
|
|
163
|
-
this.messages.push({
|
|
164
|
-
role,
|
|
165
|
-
content,
|
|
166
|
-
timestamp: new Date().toISOString(),
|
|
167
|
-
});
|
|
168
|
-
this.lastActivity = new Date();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Get conversation history
|
|
173
|
-
* @returns {Array}
|
|
174
|
-
*/
|
|
175
|
-
getHistory() {
|
|
176
|
-
return this.messages;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Update session state
|
|
181
|
-
* @param {string} state - New state (connected, thinking, idle, error)
|
|
182
|
-
*/
|
|
183
|
-
setState(state) {
|
|
184
|
-
this.state = state;
|
|
185
|
-
this.send(
|
|
186
|
-
getProtocol().createSessionState(this.id, state, {
|
|
187
|
-
messageCount: this.messages.length,
|
|
188
|
-
lastActivity: this.lastActivity.toISOString(),
|
|
189
|
-
})
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Terminal instance for integrated terminal
|
|
196
|
-
*/
|
|
197
|
-
class TerminalInstance {
|
|
198
|
-
constructor(id, session, options = {}) {
|
|
199
|
-
this.id = id;
|
|
200
|
-
this.session = session;
|
|
201
|
-
this.cwd = options.cwd || session.projectRoot;
|
|
202
|
-
this.shell = options.shell || this.getDefaultShell();
|
|
203
|
-
this.cols = options.cols || 80;
|
|
204
|
-
this.rows = options.rows || 24;
|
|
205
|
-
this.pty = null;
|
|
206
|
-
this.closed = false;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get the default shell for the current OS
|
|
211
|
-
*/
|
|
212
|
-
getDefaultShell() {
|
|
213
|
-
if (process.platform === 'win32') {
|
|
214
|
-
return process.env.COMSPEC || 'cmd.exe';
|
|
215
|
-
}
|
|
216
|
-
return process.env.SHELL || '/bin/bash';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Start the terminal process
|
|
221
|
-
*/
|
|
222
|
-
/**
|
|
223
|
-
* Get a filtered copy of environment variables with secrets removed
|
|
224
|
-
*/
|
|
225
|
-
_getFilteredEnv() {
|
|
226
|
-
const filtered = {};
|
|
227
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
228
|
-
if (!SENSITIVE_ENV_PATTERNS.test(key)) {
|
|
229
|
-
filtered[key] = value;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return filtered;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
start() {
|
|
236
|
-
try {
|
|
237
|
-
// Try to use node-pty for proper PTY support
|
|
238
|
-
const pty = require('node-pty');
|
|
239
|
-
const filteredEnv = this._getFilteredEnv();
|
|
240
|
-
|
|
241
|
-
this.pty = pty.spawn(this.shell, [], {
|
|
242
|
-
name: 'xterm-256color',
|
|
243
|
-
cols: this.cols,
|
|
244
|
-
rows: this.rows,
|
|
245
|
-
cwd: this.cwd,
|
|
246
|
-
env: {
|
|
247
|
-
...filteredEnv,
|
|
248
|
-
TERM: 'xterm-256color',
|
|
249
|
-
COLORTERM: 'truecolor',
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
this.pty.onData(data => {
|
|
254
|
-
if (!this.closed) {
|
|
255
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data));
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
this.pty.onExit(({ exitCode }) => {
|
|
260
|
-
this.closed = true;
|
|
261
|
-
this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
return true;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
// Fallback to basic spawn if node-pty is not available
|
|
267
|
-
console.warn('[Terminal] node-pty not available, using basic spawn:', error.message);
|
|
268
|
-
return this.startBasicShell();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Fallback shell using basic spawn (no PTY)
|
|
274
|
-
* Note: This provides limited functionality without node-pty
|
|
275
|
-
*/
|
|
276
|
-
startBasicShell() {
|
|
277
|
-
try {
|
|
278
|
-
const filteredEnv = this._getFilteredEnv();
|
|
279
|
-
|
|
280
|
-
// Use bash with interactive flag for better compatibility
|
|
281
|
-
this.pty = getChildProcess().spawn(this.shell, ['-i'], {
|
|
282
|
-
cwd: this.cwd,
|
|
283
|
-
env: {
|
|
284
|
-
...filteredEnv,
|
|
285
|
-
TERM: 'dumb',
|
|
286
|
-
PS1: '\\w $ ', // Simple prompt
|
|
287
|
-
},
|
|
288
|
-
shell: false,
|
|
289
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Track input buffer for local echo (since no PTY)
|
|
293
|
-
this.inputBuffer = '';
|
|
294
|
-
|
|
295
|
-
this.pty.stdout.on('data', data => {
|
|
296
|
-
if (!this.closed) {
|
|
297
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
this.pty.stderr.on('data', data => {
|
|
302
|
-
if (!this.closed) {
|
|
303
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
this.pty.on('close', exitCode => {
|
|
308
|
-
this.closed = true;
|
|
309
|
-
this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
this.pty.on('error', error => {
|
|
313
|
-
console.error('[Terminal] Shell error:', error.message);
|
|
314
|
-
if (!this.closed) {
|
|
315
|
-
this.session.send(
|
|
316
|
-
getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`)
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Send welcome message
|
|
322
|
-
setTimeout(() => {
|
|
323
|
-
if (!this.closed) {
|
|
324
|
-
const welcomeMsg = `\x1b[32mAgileFlow Terminal\x1b[0m (basic mode - node-pty not available)\r\n`;
|
|
325
|
-
const cwdMsg = `Working directory: ${this.cwd}\r\n\r\n`;
|
|
326
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, welcomeMsg + cwdMsg));
|
|
327
|
-
}
|
|
328
|
-
}, 100);
|
|
329
|
-
|
|
330
|
-
return true;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
console.error('[Terminal] Failed to start basic shell:', error.message);
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Write data to the terminal
|
|
339
|
-
* @param {string} data - Data to write (user input)
|
|
340
|
-
*/
|
|
341
|
-
write(data) {
|
|
342
|
-
if (this.pty && !this.closed) {
|
|
343
|
-
if (this.pty.write) {
|
|
344
|
-
// node-pty style - has built-in echo
|
|
345
|
-
this.pty.write(data);
|
|
346
|
-
} else if (this.pty.stdin) {
|
|
347
|
-
// basic spawn style - need manual echo
|
|
348
|
-
// Echo the input back to the terminal (since no PTY)
|
|
349
|
-
let echoData = data;
|
|
350
|
-
|
|
351
|
-
// Handle special characters
|
|
352
|
-
if (data === '\r' || data === '\n') {
|
|
353
|
-
echoData = '\r\n';
|
|
354
|
-
} else if (data === '\x7f' || data === '\b') {
|
|
355
|
-
// Backspace - move cursor back and clear
|
|
356
|
-
echoData = '\b \b';
|
|
357
|
-
} else if (data === '\x03') {
|
|
358
|
-
// Ctrl+C
|
|
359
|
-
echoData = '^C\r\n';
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Echo to terminal
|
|
363
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, echoData));
|
|
364
|
-
|
|
365
|
-
// Send to shell stdin
|
|
366
|
-
this.pty.stdin.write(data);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Resize the terminal
|
|
373
|
-
* @param {number} cols - New column count
|
|
374
|
-
* @param {number} rows - New row count
|
|
375
|
-
*/
|
|
376
|
-
resize(cols, rows) {
|
|
377
|
-
this.cols = cols;
|
|
378
|
-
this.rows = rows;
|
|
379
|
-
if (this.pty && this.pty.resize && !this.closed) {
|
|
380
|
-
this.pty.resize(cols, rows);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Close the terminal
|
|
386
|
-
*/
|
|
387
|
-
close() {
|
|
388
|
-
this.closed = true;
|
|
389
|
-
if (this.pty) {
|
|
390
|
-
if (this.pty.kill) {
|
|
391
|
-
this.pty.kill();
|
|
392
|
-
} else if (this.pty.destroy) {
|
|
393
|
-
this.pty.destroy();
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Terminal manager for handling multiple terminals per session
|
|
401
|
-
*/
|
|
402
|
-
class TerminalManager {
|
|
403
|
-
constructor() {
|
|
404
|
-
this.terminals = new Map();
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Create a new terminal for a session
|
|
409
|
-
* @param {DashboardSession} session - The session
|
|
410
|
-
* @param {Object} options - Terminal options
|
|
411
|
-
* @returns {string} - Terminal ID
|
|
412
|
-
*/
|
|
413
|
-
createTerminal(session, options = {}) {
|
|
414
|
-
const terminalId = options.id || getCrypto().randomBytes(8).toString('hex');
|
|
415
|
-
const terminal = new TerminalInstance(terminalId, session, {
|
|
416
|
-
cwd: options.cwd || session.projectRoot,
|
|
417
|
-
cols: options.cols,
|
|
418
|
-
rows: options.rows,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (terminal.start()) {
|
|
422
|
-
this.terminals.set(terminalId, terminal);
|
|
423
|
-
console.log(`[Terminal ${terminalId}] Created for session ${session.id}`);
|
|
424
|
-
return terminalId;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Get a terminal by ID
|
|
432
|
-
* @param {string} terminalId - Terminal ID
|
|
433
|
-
* @returns {TerminalInstance | undefined}
|
|
434
|
-
*/
|
|
435
|
-
getTerminal(terminalId) {
|
|
436
|
-
return this.terminals.get(terminalId);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Write to a terminal
|
|
441
|
-
* @param {string} terminalId - Terminal ID
|
|
442
|
-
* @param {string} data - Data to write
|
|
443
|
-
*/
|
|
444
|
-
writeToTerminal(terminalId, data) {
|
|
445
|
-
const terminal = this.terminals.get(terminalId);
|
|
446
|
-
if (terminal) {
|
|
447
|
-
terminal.write(data);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Resize a terminal
|
|
453
|
-
* @param {string} terminalId - Terminal ID
|
|
454
|
-
* @param {number} cols - New columns
|
|
455
|
-
* @param {number} rows - New rows
|
|
456
|
-
*/
|
|
457
|
-
resizeTerminal(terminalId, cols, rows) {
|
|
458
|
-
const terminal = this.terminals.get(terminalId);
|
|
459
|
-
if (terminal) {
|
|
460
|
-
terminal.resize(cols, rows);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Close a terminal
|
|
466
|
-
* @param {string} terminalId - Terminal ID
|
|
467
|
-
*/
|
|
468
|
-
closeTerminal(terminalId) {
|
|
469
|
-
const terminal = this.terminals.get(terminalId);
|
|
470
|
-
if (terminal) {
|
|
471
|
-
terminal.close();
|
|
472
|
-
this.terminals.delete(terminalId);
|
|
473
|
-
console.log(`[Terminal ${terminalId}] Closed`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Close all terminals for a session
|
|
479
|
-
* @param {string} sessionId - Session ID
|
|
480
|
-
*/
|
|
481
|
-
closeSessionTerminals(sessionId) {
|
|
482
|
-
for (const [terminalId, terminal] of this.terminals) {
|
|
483
|
-
if (terminal.session.id === sessionId) {
|
|
484
|
-
terminal.close();
|
|
485
|
-
this.terminals.delete(terminalId);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Get all terminals for a session
|
|
492
|
-
* @param {string} sessionId - Session ID
|
|
493
|
-
* @returns {Array<string>} - Terminal IDs
|
|
494
|
-
*/
|
|
495
|
-
getSessionTerminals(sessionId) {
|
|
496
|
-
const terminalIds = [];
|
|
497
|
-
for (const [terminalId, terminal] of this.terminals) {
|
|
498
|
-
if (terminal.session.id === sessionId) {
|
|
499
|
-
terminalIds.push(terminalId);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return terminalIds;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
102
|
/**
|
|
507
103
|
* Dashboard WebSocket Server
|
|
508
104
|
*/
|
|
@@ -592,31 +188,11 @@ class DashboardServer extends EventEmitter {
|
|
|
592
188
|
|
|
593
189
|
/**
|
|
594
190
|
* Add an automation result to the inbox
|
|
595
|
-
* @param {string} automationId - Automation ID
|
|
596
|
-
* @param {Object} result - Run result
|
|
597
191
|
*/
|
|
598
192
|
_addToInbox(automationId, result) {
|
|
599
193
|
const automation = this._automationRegistry?.get(automationId);
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
const item = {
|
|
603
|
-
id: itemId,
|
|
604
|
-
automationId,
|
|
605
|
-
title: automation?.name || automationId,
|
|
606
|
-
summary: result.success
|
|
607
|
-
? result.output?.slice(0, 200) || 'Completed successfully'
|
|
608
|
-
: result.error?.slice(0, 200) || 'Failed',
|
|
609
|
-
timestamp: new Date().toISOString(),
|
|
610
|
-
status: 'unread',
|
|
611
|
-
result: {
|
|
612
|
-
success: result.success,
|
|
613
|
-
output: result.output,
|
|
614
|
-
error: result.error,
|
|
615
|
-
duration_ms: result.duration_ms,
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
this._inbox.set(itemId, item);
|
|
194
|
+
const item = createInboxItem(automationId, result, automation?.name);
|
|
195
|
+
this._inbox.set(item.id, item);
|
|
620
196
|
this.broadcast(getProtocol().createInboxItem(item));
|
|
621
197
|
}
|
|
622
198
|
|
|
@@ -803,7 +379,11 @@ class DashboardServer extends EventEmitter {
|
|
|
803
379
|
session = new DashboardSession(sessionId, socket, this.projectRoot);
|
|
804
380
|
this.sessions.set(sessionId, session);
|
|
805
381
|
} else {
|
|
806
|
-
//
|
|
382
|
+
// Clean up old socket before replacing
|
|
383
|
+
if (session.ws && session.ws !== socket) {
|
|
384
|
+
session.ws.removeAllListeners();
|
|
385
|
+
session.ws.destroy();
|
|
386
|
+
}
|
|
807
387
|
session.ws = socket;
|
|
808
388
|
}
|
|
809
389
|
|
|
@@ -1044,78 +624,18 @@ class DashboardServer extends EventEmitter {
|
|
|
1044
624
|
}
|
|
1045
625
|
}
|
|
1046
626
|
|
|
627
|
+
// ==========================================================================
|
|
628
|
+
// Git Handlers (delegating to dashboard-git.js)
|
|
629
|
+
// ==========================================================================
|
|
630
|
+
|
|
1047
631
|
/**
|
|
1048
632
|
* Handle git actions
|
|
1049
633
|
*/
|
|
1050
634
|
handleGitAction(session, message) {
|
|
1051
635
|
const { type, files, message: commitMessage } = message;
|
|
1052
636
|
|
|
1053
|
-
// Validate file paths - reject path traversal attempts
|
|
1054
|
-
if (files && files.length > 0) {
|
|
1055
|
-
for (const f of files) {
|
|
1056
|
-
if (typeof f !== 'string' || f.includes('\0')) {
|
|
1057
|
-
session.send(getProtocol().createError('GIT_ERROR', 'Invalid file path'));
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const resolved = require('path').resolve(this.projectRoot, f);
|
|
1061
|
-
if (!resolved.startsWith(this.projectRoot)) {
|
|
1062
|
-
session.send(getProtocol().createError('GIT_ERROR', 'File path outside project'));
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Validate commit message
|
|
1069
|
-
if (commitMessage !== undefined && commitMessage !== null) {
|
|
1070
|
-
if (
|
|
1071
|
-
typeof commitMessage !== 'string' ||
|
|
1072
|
-
commitMessage.length > 10000 ||
|
|
1073
|
-
commitMessage.includes('\0')
|
|
1074
|
-
) {
|
|
1075
|
-
session.send(getProtocol().createError('GIT_ERROR', 'Invalid commit message'));
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
const fileArgs = files && files.length > 0 ? files : null;
|
|
1081
|
-
|
|
1082
637
|
try {
|
|
1083
|
-
|
|
1084
|
-
case getProtocol().InboundMessageType.GIT_STAGE:
|
|
1085
|
-
if (fileArgs) {
|
|
1086
|
-
getChildProcess().execFileSync('git', ['add', '--', ...fileArgs], {
|
|
1087
|
-
cwd: this.projectRoot,
|
|
1088
|
-
});
|
|
1089
|
-
} else {
|
|
1090
|
-
getChildProcess().execFileSync('git', ['add', '-A'], { cwd: this.projectRoot });
|
|
1091
|
-
}
|
|
1092
|
-
break;
|
|
1093
|
-
case getProtocol().InboundMessageType.GIT_UNSTAGE:
|
|
1094
|
-
if (fileArgs) {
|
|
1095
|
-
getChildProcess().execFileSync('git', ['restore', '--staged', '--', ...fileArgs], {
|
|
1096
|
-
cwd: this.projectRoot,
|
|
1097
|
-
});
|
|
1098
|
-
} else {
|
|
1099
|
-
getChildProcess().execFileSync('git', ['restore', '--staged', '.'], {
|
|
1100
|
-
cwd: this.projectRoot,
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
break;
|
|
1104
|
-
case getProtocol().InboundMessageType.GIT_REVERT:
|
|
1105
|
-
if (fileArgs) {
|
|
1106
|
-
getChildProcess().execFileSync('git', ['checkout', '--', ...fileArgs], {
|
|
1107
|
-
cwd: this.projectRoot,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
break;
|
|
1111
|
-
case getProtocol().InboundMessageType.GIT_COMMIT:
|
|
1112
|
-
if (commitMessage) {
|
|
1113
|
-
getChildProcess().execFileSync('git', ['commit', '-m', commitMessage], {
|
|
1114
|
-
cwd: this.projectRoot,
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
break;
|
|
1118
|
-
}
|
|
638
|
+
handleGitAction(type, this.projectRoot, { files, commitMessage }, getProtocol());
|
|
1119
639
|
|
|
1120
640
|
// Send updated git status
|
|
1121
641
|
this.sendGitStatus(session);
|
|
@@ -1124,7 +644,7 @@ class DashboardServer extends EventEmitter {
|
|
|
1124
644
|
);
|
|
1125
645
|
} catch (error) {
|
|
1126
646
|
console.error('[Git Error]', error.message);
|
|
1127
|
-
session.send(getProtocol().createError('GIT_ERROR', 'Git operation failed'));
|
|
647
|
+
session.send(getProtocol().createError('GIT_ERROR', error.message || 'Git operation failed'));
|
|
1128
648
|
}
|
|
1129
649
|
}
|
|
1130
650
|
|
|
@@ -1133,7 +653,7 @@ class DashboardServer extends EventEmitter {
|
|
|
1133
653
|
*/
|
|
1134
654
|
sendGitStatus(session) {
|
|
1135
655
|
try {
|
|
1136
|
-
const status = this.
|
|
656
|
+
const status = getGitStatus(this.projectRoot);
|
|
1137
657
|
session.send({
|
|
1138
658
|
type: getProtocol().OutboundMessageType.GIT_STATUS,
|
|
1139
659
|
...status,
|
|
@@ -1144,71 +664,6 @@ class DashboardServer extends EventEmitter {
|
|
|
1144
664
|
}
|
|
1145
665
|
}
|
|
1146
666
|
|
|
1147
|
-
/**
|
|
1148
|
-
* Get current git status
|
|
1149
|
-
*/
|
|
1150
|
-
getGitStatus() {
|
|
1151
|
-
try {
|
|
1152
|
-
const branch = getChildProcess()
|
|
1153
|
-
.execFileSync('git', ['branch', '--show-current'], {
|
|
1154
|
-
cwd: this.projectRoot,
|
|
1155
|
-
encoding: 'utf8',
|
|
1156
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1157
|
-
})
|
|
1158
|
-
.trim();
|
|
1159
|
-
|
|
1160
|
-
const statusOutput = getChildProcess().execFileSync('git', ['status', '--porcelain'], {
|
|
1161
|
-
cwd: this.projectRoot,
|
|
1162
|
-
encoding: 'utf8',
|
|
1163
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
const staged = [];
|
|
1167
|
-
const unstaged = [];
|
|
1168
|
-
|
|
1169
|
-
for (const line of statusOutput.split('\n').filter(Boolean)) {
|
|
1170
|
-
const indexStatus = line[0];
|
|
1171
|
-
const workTreeStatus = line[1];
|
|
1172
|
-
const file = line.slice(3);
|
|
1173
|
-
|
|
1174
|
-
// Parse the status character to a descriptive status
|
|
1175
|
-
const parseStatus = char => {
|
|
1176
|
-
switch (char) {
|
|
1177
|
-
case 'A':
|
|
1178
|
-
return 'added';
|
|
1179
|
-
case 'M':
|
|
1180
|
-
return 'modified';
|
|
1181
|
-
case 'D':
|
|
1182
|
-
return 'deleted';
|
|
1183
|
-
case 'R':
|
|
1184
|
-
return 'renamed';
|
|
1185
|
-
case 'C':
|
|
1186
|
-
return 'copied';
|
|
1187
|
-
case '?':
|
|
1188
|
-
return 'untracked';
|
|
1189
|
-
default:
|
|
1190
|
-
return 'modified';
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
|
|
1194
|
-
if (indexStatus !== ' ' && indexStatus !== '?') {
|
|
1195
|
-
staged.push({ path: file, file, status: parseStatus(indexStatus) });
|
|
1196
|
-
}
|
|
1197
|
-
if (workTreeStatus !== ' ') {
|
|
1198
|
-
unstaged.push({
|
|
1199
|
-
path: file,
|
|
1200
|
-
file,
|
|
1201
|
-
status: workTreeStatus === '?' ? 'untracked' : parseStatus(workTreeStatus),
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
return { branch, staged, unstaged };
|
|
1207
|
-
} catch {
|
|
1208
|
-
return { branch: 'unknown', staged: [], unstaged: [] };
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
667
|
/**
|
|
1213
668
|
* Handle diff request for a file
|
|
1214
669
|
*/
|
|
@@ -1221,8 +676,8 @@ class DashboardServer extends EventEmitter {
|
|
|
1221
676
|
}
|
|
1222
677
|
|
|
1223
678
|
try {
|
|
1224
|
-
const diff =
|
|
1225
|
-
const stats =
|
|
679
|
+
const diff = getFileDiff(filePath, this.projectRoot, staged);
|
|
680
|
+
const stats = parseDiffStats(diff);
|
|
1226
681
|
|
|
1227
682
|
session.send(
|
|
1228
683
|
getProtocol().createGitDiff(filePath, diff, {
|
|
@@ -1237,123 +692,19 @@ class DashboardServer extends EventEmitter {
|
|
|
1237
692
|
}
|
|
1238
693
|
}
|
|
1239
694
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
* @param {boolean} staged - Whether to get staged diff
|
|
1244
|
-
* @returns {string} - The diff content
|
|
1245
|
-
*/
|
|
1246
|
-
getFileDiff(filePath, staged = false) {
|
|
1247
|
-
// Validate filePath stays within project root
|
|
1248
|
-
const pathResult = getValidatePaths().validatePath(filePath, this.projectRoot, {
|
|
1249
|
-
allowSymlinks: true,
|
|
1250
|
-
});
|
|
1251
|
-
if (!pathResult.ok) {
|
|
1252
|
-
return '';
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
try {
|
|
1256
|
-
const diffArgs = staged ? ['diff', '--cached', '--', filePath] : ['diff', '--', filePath];
|
|
1257
|
-
|
|
1258
|
-
const diff = getChildProcess().execFileSync('git', diffArgs, {
|
|
1259
|
-
cwd: this.projectRoot,
|
|
1260
|
-
encoding: 'utf8',
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
// If no diff, file might be untracked - show entire file content as addition
|
|
1264
|
-
if (!diff && !staged) {
|
|
1265
|
-
const statusOutput = getChildProcess()
|
|
1266
|
-
.execFileSync('git', ['status', '--porcelain', '--', filePath], {
|
|
1267
|
-
cwd: this.projectRoot,
|
|
1268
|
-
encoding: 'utf8',
|
|
1269
|
-
})
|
|
1270
|
-
.trim();
|
|
1271
|
-
|
|
1272
|
-
// Check if file is untracked
|
|
1273
|
-
if (statusOutput.startsWith('??')) {
|
|
1274
|
-
try {
|
|
1275
|
-
const content = require('fs').readFileSync(
|
|
1276
|
-
require('path').join(this.projectRoot, filePath),
|
|
1277
|
-
'utf8'
|
|
1278
|
-
);
|
|
1279
|
-
// Format as a new file diff
|
|
1280
|
-
const lines = content.split('\n');
|
|
1281
|
-
return [
|
|
1282
|
-
`diff --git a/${filePath} b/${filePath}`,
|
|
1283
|
-
`new file mode 100644`,
|
|
1284
|
-
`--- /dev/null`,
|
|
1285
|
-
`+++ b/${filePath}`,
|
|
1286
|
-
`@@ -0,0 +1,${lines.length} @@`,
|
|
1287
|
-
...lines.map(line => `+${line}`),
|
|
1288
|
-
].join('\n');
|
|
1289
|
-
} catch {
|
|
1290
|
-
return '';
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return diff;
|
|
1296
|
-
} catch (error) {
|
|
1297
|
-
console.error('[Diff Error]', error.message);
|
|
1298
|
-
return '';
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
/**
|
|
1303
|
-
* Parse diff statistics from diff content
|
|
1304
|
-
* @param {string} diff - The diff content
|
|
1305
|
-
* @returns {{ additions: number, deletions: number }}
|
|
1306
|
-
*/
|
|
1307
|
-
parseDiffStats(diff) {
|
|
1308
|
-
let additions = 0;
|
|
1309
|
-
let deletions = 0;
|
|
1310
|
-
|
|
1311
|
-
for (const line of diff.split('\n')) {
|
|
1312
|
-
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
1313
|
-
additions++;
|
|
1314
|
-
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
1315
|
-
deletions++;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
return { additions, deletions };
|
|
1320
|
-
}
|
|
695
|
+
// ==========================================================================
|
|
696
|
+
// Status/Metrics Handlers (delegating to dashboard-status.js)
|
|
697
|
+
// ==========================================================================
|
|
1321
698
|
|
|
1322
699
|
/**
|
|
1323
700
|
* Send project status update (stories/epics summary) to session
|
|
1324
701
|
*/
|
|
1325
702
|
sendStatusUpdate(session) {
|
|
1326
|
-
const path = require('path');
|
|
1327
|
-
const fs = require('fs');
|
|
1328
|
-
const statusPath = path.join(this.projectRoot, 'docs', '09-agents', 'status.json');
|
|
1329
|
-
if (!fs.existsSync(statusPath)) return;
|
|
1330
|
-
|
|
1331
703
|
try {
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const storyValues = Object.values(stories);
|
|
1337
|
-
const summary = {
|
|
1338
|
-
total: storyValues.length,
|
|
1339
|
-
done: storyValues.filter(s => s.status === 'done' || s.status === 'completed').length,
|
|
1340
|
-
inProgress: storyValues.filter(s => s.status === 'in-progress').length,
|
|
1341
|
-
ready: storyValues.filter(s => s.status === 'ready').length,
|
|
1342
|
-
blocked: storyValues.filter(s => s.status === 'blocked').length,
|
|
1343
|
-
epics: Object.entries(epics).map(([id, e]) => ({
|
|
1344
|
-
id,
|
|
1345
|
-
title: e.title || id,
|
|
1346
|
-
status: e.status || 'unknown',
|
|
1347
|
-
storyCount: (e.stories || []).length,
|
|
1348
|
-
doneCount: (e.stories || []).filter(
|
|
1349
|
-
sid =>
|
|
1350
|
-
stories[sid] &&
|
|
1351
|
-
(stories[sid].status === 'done' || stories[sid].status === 'completed')
|
|
1352
|
-
).length,
|
|
1353
|
-
})),
|
|
1354
|
-
};
|
|
1355
|
-
|
|
1356
|
-
session.send(getProtocol().createStatusUpdate(summary));
|
|
704
|
+
const summary = buildStatusSummary(this.projectRoot);
|
|
705
|
+
if (summary) {
|
|
706
|
+
session.send(getProtocol().createStatusUpdate(summary));
|
|
707
|
+
}
|
|
1357
708
|
} catch (error) {
|
|
1358
709
|
console.error('[Status Update Error]', error.message);
|
|
1359
710
|
}
|
|
@@ -1365,9 +716,10 @@ class DashboardServer extends EventEmitter {
|
|
|
1365
716
|
_initTeamMetricsListener() {
|
|
1366
717
|
try {
|
|
1367
718
|
const { teamMetricsEmitter } = require('../scripts/lib/team-events');
|
|
1368
|
-
|
|
719
|
+
this._teamMetricsListener = () => {
|
|
1369
720
|
this.broadcastTeamMetrics();
|
|
1370
|
-
}
|
|
721
|
+
};
|
|
722
|
+
teamMetricsEmitter.on('metrics_saved', this._teamMetricsListener);
|
|
1371
723
|
} catch (e) {
|
|
1372
724
|
// team-events not available - non-critical
|
|
1373
725
|
}
|
|
@@ -1377,20 +729,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1377
729
|
* Send team metrics to a single session
|
|
1378
730
|
*/
|
|
1379
731
|
sendTeamMetrics(session) {
|
|
1380
|
-
const
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
if (!fs.existsSync(sessionStatePath)) return;
|
|
1384
|
-
|
|
1385
|
-
try {
|
|
1386
|
-
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
1387
|
-
const traces = (state.team_metrics && state.team_metrics.traces) || {};
|
|
1388
|
-
|
|
1389
|
-
for (const [traceId, metrics] of Object.entries(traces)) {
|
|
1390
|
-
session.send(getProtocol().createTeamMetrics(traceId, metrics));
|
|
1391
|
-
}
|
|
1392
|
-
} catch (error) {
|
|
1393
|
-
// Non-critical
|
|
732
|
+
const traces = readTeamMetrics(this.projectRoot);
|
|
733
|
+
for (const [traceId, metrics] of Object.entries(traces)) {
|
|
734
|
+
session.send(getProtocol().createTeamMetrics(traceId, metrics));
|
|
1394
735
|
}
|
|
1395
736
|
}
|
|
1396
737
|
|
|
@@ -1398,20 +739,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1398
739
|
* Broadcast team metrics to all connected clients
|
|
1399
740
|
*/
|
|
1400
741
|
broadcastTeamMetrics() {
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
if (!fs.existsSync(sessionStatePath)) return;
|
|
1405
|
-
|
|
1406
|
-
try {
|
|
1407
|
-
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
1408
|
-
const traces = (state.team_metrics && state.team_metrics.traces) || {};
|
|
1409
|
-
|
|
1410
|
-
for (const [traceId, metrics] of Object.entries(traces)) {
|
|
1411
|
-
this.broadcast(getProtocol().createTeamMetrics(traceId, metrics));
|
|
1412
|
-
}
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
// Non-critical
|
|
742
|
+
const traces = readTeamMetrics(this.projectRoot);
|
|
743
|
+
for (const [traceId, metrics] of Object.entries(traces)) {
|
|
744
|
+
this.broadcast(getProtocol().createTeamMetrics(traceId, metrics));
|
|
1415
745
|
}
|
|
1416
746
|
}
|
|
1417
747
|
|
|
@@ -1549,6 +879,10 @@ class DashboardServer extends EventEmitter {
|
|
|
1549
879
|
}
|
|
1550
880
|
}
|
|
1551
881
|
|
|
882
|
+
// ==========================================================================
|
|
883
|
+
// Terminal Handlers (delegating to dashboard-terminal.js)
|
|
884
|
+
// ==========================================================================
|
|
885
|
+
|
|
1552
886
|
/**
|
|
1553
887
|
* Handle terminal spawn request
|
|
1554
888
|
*/
|
|
@@ -1631,7 +965,7 @@ class DashboardServer extends EventEmitter {
|
|
|
1631
965
|
}
|
|
1632
966
|
|
|
1633
967
|
// ==========================================================================
|
|
1634
|
-
// Automation Handlers
|
|
968
|
+
// Automation Handlers (delegating to dashboard-automations.js)
|
|
1635
969
|
// ==========================================================================
|
|
1636
970
|
|
|
1637
971
|
/**
|
|
@@ -1644,23 +978,12 @@ class DashboardServer extends EventEmitter {
|
|
|
1644
978
|
}
|
|
1645
979
|
|
|
1646
980
|
try {
|
|
1647
|
-
const automations = this._automationRegistry.list();
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
const nextRun = this._calculateNextRun(automation);
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
...automation,
|
|
1657
|
-
status: isRunning ? 'running' : automation.enabled ? 'idle' : 'disabled',
|
|
1658
|
-
lastRun: lastRun?.at,
|
|
1659
|
-
lastRunSuccess: lastRun?.success,
|
|
1660
|
-
nextRun,
|
|
1661
|
-
};
|
|
1662
|
-
});
|
|
1663
|
-
|
|
981
|
+
const automations = this._automationRegistry.list() || [];
|
|
982
|
+
const enriched = enrichAutomationList(
|
|
983
|
+
automations,
|
|
984
|
+
this._runningAutomations,
|
|
985
|
+
this._automationRegistry
|
|
986
|
+
);
|
|
1664
987
|
session.send(getProtocol().createAutomationList(enriched));
|
|
1665
988
|
} catch (error) {
|
|
1666
989
|
console.error('[Automations] List error:', error.message);
|
|
@@ -1668,65 +991,6 @@ class DashboardServer extends EventEmitter {
|
|
|
1668
991
|
}
|
|
1669
992
|
}
|
|
1670
993
|
|
|
1671
|
-
/**
|
|
1672
|
-
* Calculate next run time for an automation
|
|
1673
|
-
*/
|
|
1674
|
-
_calculateNextRun(automation) {
|
|
1675
|
-
if (!automation.enabled || !automation.schedule) return null;
|
|
1676
|
-
|
|
1677
|
-
const now = new Date();
|
|
1678
|
-
const schedule = automation.schedule;
|
|
1679
|
-
|
|
1680
|
-
switch (schedule.type) {
|
|
1681
|
-
case 'on_session':
|
|
1682
|
-
return 'Every session';
|
|
1683
|
-
case 'daily': {
|
|
1684
|
-
// Next day at midnight (or specified hour)
|
|
1685
|
-
const nextDaily = new Date(now);
|
|
1686
|
-
nextDaily.setDate(nextDaily.getDate() + 1);
|
|
1687
|
-
nextDaily.setHours(schedule.hour || 0, 0, 0, 0);
|
|
1688
|
-
return nextDaily.toISOString();
|
|
1689
|
-
}
|
|
1690
|
-
case 'weekly': {
|
|
1691
|
-
// Next occurrence of the specified day
|
|
1692
|
-
const targetDay =
|
|
1693
|
-
typeof schedule.day === 'string'
|
|
1694
|
-
? [
|
|
1695
|
-
'sunday',
|
|
1696
|
-
'monday',
|
|
1697
|
-
'tuesday',
|
|
1698
|
-
'wednesday',
|
|
1699
|
-
'thursday',
|
|
1700
|
-
'friday',
|
|
1701
|
-
'saturday',
|
|
1702
|
-
].indexOf(schedule.day.toLowerCase())
|
|
1703
|
-
: schedule.day || 0;
|
|
1704
|
-
const nextWeekly = new Date(now);
|
|
1705
|
-
const daysUntil = (targetDay - now.getDay() + 7) % 7 || 7;
|
|
1706
|
-
nextWeekly.setDate(nextWeekly.getDate() + daysUntil);
|
|
1707
|
-
nextWeekly.setHours(schedule.hour || 0, 0, 0, 0);
|
|
1708
|
-
return nextWeekly.toISOString();
|
|
1709
|
-
}
|
|
1710
|
-
case 'monthly': {
|
|
1711
|
-
// Next occurrence of the specified date
|
|
1712
|
-
const nextMonthly = new Date(now);
|
|
1713
|
-
const targetDate = schedule.date || 1;
|
|
1714
|
-
if (now.getDate() >= targetDate) {
|
|
1715
|
-
nextMonthly.setMonth(nextMonthly.getMonth() + 1);
|
|
1716
|
-
}
|
|
1717
|
-
nextMonthly.setDate(targetDate);
|
|
1718
|
-
nextMonthly.setHours(schedule.hour || 0, 0, 0, 0);
|
|
1719
|
-
return nextMonthly.toISOString();
|
|
1720
|
-
}
|
|
1721
|
-
case 'interval': {
|
|
1722
|
-
const hours = schedule.hours || 24;
|
|
1723
|
-
return `Every ${hours} hour${hours > 1 ? 's' : ''}`;
|
|
1724
|
-
}
|
|
1725
|
-
default:
|
|
1726
|
-
return null;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
994
|
/**
|
|
1731
995
|
* Handle automation run request
|
|
1732
996
|
*/
|
|
@@ -1758,6 +1022,9 @@ class DashboardServer extends EventEmitter {
|
|
|
1758
1022
|
return;
|
|
1759
1023
|
}
|
|
1760
1024
|
|
|
1025
|
+
// Mark as running BEFORE the async call to prevent duplicate execution
|
|
1026
|
+
this._runningAutomations.set(automationId, { startTime: Date.now() });
|
|
1027
|
+
|
|
1761
1028
|
session.send(
|
|
1762
1029
|
getProtocol().createNotification('info', 'Automation', `Starting ${automationId}...`)
|
|
1763
1030
|
);
|
|
@@ -1826,17 +1093,14 @@ class DashboardServer extends EventEmitter {
|
|
|
1826
1093
|
}
|
|
1827
1094
|
|
|
1828
1095
|
// ==========================================================================
|
|
1829
|
-
// Inbox Handlers
|
|
1096
|
+
// Inbox Handlers (delegating to dashboard-inbox.js)
|
|
1830
1097
|
// ==========================================================================
|
|
1831
1098
|
|
|
1832
1099
|
/**
|
|
1833
1100
|
* Send inbox list to session
|
|
1834
1101
|
*/
|
|
1835
1102
|
sendInboxList(session) {
|
|
1836
|
-
const items =
|
|
1837
|
-
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
|
|
1838
|
-
);
|
|
1839
|
-
|
|
1103
|
+
const items = getSortedInboxItems(this._inbox);
|
|
1840
1104
|
session.send(getProtocol().createInboxList(items));
|
|
1841
1105
|
}
|
|
1842
1106
|
|
|
@@ -1851,53 +1115,47 @@ class DashboardServer extends EventEmitter {
|
|
|
1851
1115
|
return;
|
|
1852
1116
|
}
|
|
1853
1117
|
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1118
|
+
const result = handleInboxAction(this._inbox, itemId, action);
|
|
1119
|
+
|
|
1120
|
+
if (!result.success) {
|
|
1121
|
+
const errorCode = result.error.includes('not found') ? 'NOT_FOUND' : 'INVALID_ACTION';
|
|
1122
|
+
session.send(getProtocol().createError(errorCode, result.error));
|
|
1857
1123
|
return;
|
|
1858
1124
|
}
|
|
1859
1125
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
)
|
|
1867
|
-
|
|
1868
|
-
break;
|
|
1869
|
-
|
|
1870
|
-
case 'dismiss':
|
|
1871
|
-
// Mark as dismissed and remove
|
|
1872
|
-
item.status = 'dismissed';
|
|
1873
|
-
session.send(getProtocol().createNotification('info', 'Inbox', `Dismissed: ${item.title}`));
|
|
1874
|
-
this._inbox.delete(itemId);
|
|
1875
|
-
break;
|
|
1876
|
-
|
|
1877
|
-
case 'read':
|
|
1878
|
-
// Mark as read
|
|
1879
|
-
item.status = 'read';
|
|
1880
|
-
break;
|
|
1881
|
-
|
|
1882
|
-
default:
|
|
1883
|
-
session.send(getProtocol().createError('INVALID_ACTION', `Unknown action: ${action}`));
|
|
1884
|
-
return;
|
|
1126
|
+
if (result.notification) {
|
|
1127
|
+
session.send(
|
|
1128
|
+
getProtocol().createNotification(
|
|
1129
|
+
result.notification.level,
|
|
1130
|
+
'Inbox',
|
|
1131
|
+
result.notification.message
|
|
1132
|
+
)
|
|
1133
|
+
);
|
|
1885
1134
|
}
|
|
1886
1135
|
|
|
1887
1136
|
// Send updated inbox list
|
|
1888
1137
|
this.sendInboxList(session);
|
|
1889
1138
|
}
|
|
1890
1139
|
|
|
1140
|
+
// ==========================================================================
|
|
1141
|
+
// Session Lifecycle
|
|
1142
|
+
// ==========================================================================
|
|
1143
|
+
|
|
1891
1144
|
/**
|
|
1892
1145
|
* Cleanup expired sessions
|
|
1893
1146
|
*/
|
|
1894
1147
|
_cleanupExpiredSessions() {
|
|
1148
|
+
// Collect expired IDs first to avoid mutating Map during iteration
|
|
1149
|
+
const expiredIds = [];
|
|
1895
1150
|
for (const [sessionId, session] of this.sessions) {
|
|
1896
1151
|
if (session.isExpired()) {
|
|
1897
|
-
|
|
1898
|
-
this.closeSession(sessionId);
|
|
1152
|
+
expiredIds.push(sessionId);
|
|
1899
1153
|
}
|
|
1900
1154
|
}
|
|
1155
|
+
for (const sessionId of expiredIds) {
|
|
1156
|
+
console.log(`[Session ${sessionId}] Expired (idle > ${SESSION_TIMEOUT_MS / 3600000}h)`);
|
|
1157
|
+
this.closeSession(sessionId);
|
|
1158
|
+
}
|
|
1901
1159
|
}
|
|
1902
1160
|
|
|
1903
1161
|
/**
|
|
@@ -1941,6 +1199,17 @@ class DashboardServer extends EventEmitter {
|
|
|
1941
1199
|
*/
|
|
1942
1200
|
stop() {
|
|
1943
1201
|
return new Promise(resolve => {
|
|
1202
|
+
// Remove team metrics listener to prevent leak
|
|
1203
|
+
if (this._teamMetricsListener) {
|
|
1204
|
+
try {
|
|
1205
|
+
const { teamMetricsEmitter } = require('../scripts/lib/team-events');
|
|
1206
|
+
teamMetricsEmitter.removeListener('metrics_saved', this._teamMetricsListener);
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
// Ignore if module not available
|
|
1209
|
+
}
|
|
1210
|
+
this._teamMetricsListener = null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1944
1213
|
// Clear cleanup interval
|
|
1945
1214
|
if (this._cleanupInterval) {
|
|
1946
1215
|
clearInterval(this._cleanupInterval);
|
|
@@ -1968,85 +1237,6 @@ class DashboardServer extends EventEmitter {
|
|
|
1968
1237
|
}
|
|
1969
1238
|
}
|
|
1970
1239
|
|
|
1971
|
-
// ============================================================================
|
|
1972
|
-
// WebSocket Frame Encoding/Decoding
|
|
1973
|
-
// ============================================================================
|
|
1974
|
-
|
|
1975
|
-
/**
|
|
1976
|
-
* Encode a WebSocket frame
|
|
1977
|
-
* @param {string|Buffer} data - Data to encode
|
|
1978
|
-
* @param {number} [opcode=0x1] - Frame opcode (0x1 = text, 0x2 = binary)
|
|
1979
|
-
* @returns {Buffer}
|
|
1980
|
-
*/
|
|
1981
|
-
function encodeWebSocketFrame(data, opcode = 0x1) {
|
|
1982
|
-
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1983
|
-
const length = payload.length;
|
|
1984
|
-
|
|
1985
|
-
let header;
|
|
1986
|
-
if (length < 126) {
|
|
1987
|
-
header = Buffer.alloc(2);
|
|
1988
|
-
header[0] = 0x80 | opcode; // FIN + opcode
|
|
1989
|
-
header[1] = length;
|
|
1990
|
-
} else if (length < 65536) {
|
|
1991
|
-
header = Buffer.alloc(4);
|
|
1992
|
-
header[0] = 0x80 | opcode;
|
|
1993
|
-
header[1] = 126;
|
|
1994
|
-
header.writeUInt16BE(length, 2);
|
|
1995
|
-
} else {
|
|
1996
|
-
header = Buffer.alloc(10);
|
|
1997
|
-
header[0] = 0x80 | opcode;
|
|
1998
|
-
header[1] = 127;
|
|
1999
|
-
header.writeBigUInt64BE(BigInt(length), 2);
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
return Buffer.concat([header, payload]);
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
/**
|
|
2006
|
-
* Decode a WebSocket frame
|
|
2007
|
-
* @param {Buffer} buffer - Buffer containing frame data
|
|
2008
|
-
* @returns {{ opcode: number, payload: Buffer, totalLength: number } | null}
|
|
2009
|
-
*/
|
|
2010
|
-
function decodeWebSocketFrame(buffer) {
|
|
2011
|
-
if (buffer.length < 2) return null;
|
|
2012
|
-
|
|
2013
|
-
const firstByte = buffer[0];
|
|
2014
|
-
const secondByte = buffer[1];
|
|
2015
|
-
|
|
2016
|
-
const opcode = firstByte & 0x0f;
|
|
2017
|
-
const masked = (secondByte & 0x80) !== 0;
|
|
2018
|
-
let payloadLength = secondByte & 0x7f;
|
|
2019
|
-
|
|
2020
|
-
let headerLength = 2;
|
|
2021
|
-
if (payloadLength === 126) {
|
|
2022
|
-
if (buffer.length < 4) return null;
|
|
2023
|
-
payloadLength = buffer.readUInt16BE(2);
|
|
2024
|
-
headerLength = 4;
|
|
2025
|
-
} else if (payloadLength === 127) {
|
|
2026
|
-
if (buffer.length < 10) return null;
|
|
2027
|
-
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
2028
|
-
headerLength = 10;
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
if (masked) headerLength += 4;
|
|
2032
|
-
|
|
2033
|
-
const totalLength = headerLength + payloadLength;
|
|
2034
|
-
if (buffer.length < totalLength) return null;
|
|
2035
|
-
|
|
2036
|
-
let payload = buffer.slice(headerLength, totalLength);
|
|
2037
|
-
|
|
2038
|
-
// Unmask if needed
|
|
2039
|
-
if (masked) {
|
|
2040
|
-
const mask = buffer.slice(headerLength - 4, headerLength);
|
|
2041
|
-
payload = Buffer.alloc(payloadLength);
|
|
2042
|
-
for (let i = 0; i < payloadLength; i++) {
|
|
2043
|
-
payload[i] = buffer[headerLength + i] ^ mask[i % 4];
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
return { opcode, payload, totalLength };
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
1240
|
// ============================================================================
|
|
2051
1241
|
// Factory Functions
|
|
2052
1242
|
// ============================================================================
|
|
@@ -2084,7 +1274,7 @@ async function stopDashboardServer(server) {
|
|
|
2084
1274
|
}
|
|
2085
1275
|
|
|
2086
1276
|
// ============================================================================
|
|
2087
|
-
// Exports
|
|
1277
|
+
// Exports (backward-compatible - re-exports from extracted modules)
|
|
2088
1278
|
// ============================================================================
|
|
2089
1279
|
|
|
2090
1280
|
module.exports = {
|