agileflow 3.4.2 → 3.4.3
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 +5 -0
- package/README.md +2 -2
- package/lib/drivers/claude-driver.ts +1 -1
- package/lib/lazy-require.js +1 -1
- package/package.json +1 -1
- package/scripts/agent-loop.js +290 -230
- package/scripts/check-sessions.js +116 -0
- package/scripts/lib/quality-gates.js +35 -8
- package/scripts/lib/signal-detectors.js +0 -13
- package/scripts/lib/team-events.js +1 -1
- package/scripts/lib/tmux-audit-monitor.js +2 -1
- package/src/core/commands/ads/audit.md +19 -3
- package/src/core/commands/code/accessibility.md +22 -6
- package/src/core/commands/code/api.md +22 -6
- package/src/core/commands/code/architecture.md +22 -6
- package/src/core/commands/code/completeness.md +22 -6
- package/src/core/commands/code/legal.md +22 -6
- package/src/core/commands/code/logic.md +22 -6
- package/src/core/commands/code/performance.md +22 -6
- package/src/core/commands/code/security.md +22 -6
- package/src/core/commands/code/test.md +22 -6
- package/src/core/commands/ideate/features.md +5 -4
- package/src/core/commands/ideate/new.md +8 -7
- package/src/core/commands/seo/audit.md +21 -5
- package/lib/claude-cli-bridge.js +0 -215
- package/lib/dashboard-automations.js +0 -130
- package/lib/dashboard-git.js +0 -254
- package/lib/dashboard-inbox.js +0 -64
- package/lib/dashboard-protocol.js +0 -605
- package/lib/dashboard-server.js +0 -1296
- package/lib/dashboard-session.js +0 -136
- package/lib/dashboard-status.js +0 -72
- package/lib/dashboard-terminal.js +0 -354
- package/lib/dashboard-websocket.js +0 -88
- package/scripts/dashboard-serve.js +0 -336
- package/src/core/commands/serve.md +0 -127
- package/tools/cli/commands/serve.js +0 -492
package/lib/dashboard-session.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* dashboard-session.js - Dashboard Session Management
|
|
5
|
-
*
|
|
6
|
-
* Manages individual client sessions with rate limiting,
|
|
7
|
-
* message history, and state tracking.
|
|
8
|
-
* Extracted from dashboard-server.js for testability.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const { encodeWebSocketFrame } = require('./dashboard-websocket');
|
|
12
|
-
|
|
13
|
-
// Session lifecycle
|
|
14
|
-
const SESSION_TIMEOUT_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
15
|
-
const SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
16
|
-
|
|
17
|
-
// Rate limiting (token bucket)
|
|
18
|
-
const RATE_LIMIT_TOKENS = 100; // max messages per second
|
|
19
|
-
const RATE_LIMIT_REFILL_MS = 1000; // refill interval
|
|
20
|
-
|
|
21
|
-
// Lazy-loaded protocol
|
|
22
|
-
let _protocol;
|
|
23
|
-
function getProtocol() {
|
|
24
|
-
if (!_protocol) _protocol = require('./dashboard-protocol');
|
|
25
|
-
return _protocol;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Session state for a connected dashboard
|
|
30
|
-
*/
|
|
31
|
-
class DashboardSession {
|
|
32
|
-
constructor(id, ws, projectRoot) {
|
|
33
|
-
this.id = id;
|
|
34
|
-
this.ws = ws;
|
|
35
|
-
this.projectRoot = projectRoot;
|
|
36
|
-
this.messages = [];
|
|
37
|
-
this.state = 'connected';
|
|
38
|
-
this.lastActivity = new Date();
|
|
39
|
-
this.createdAt = new Date();
|
|
40
|
-
this.metadata = {};
|
|
41
|
-
|
|
42
|
-
// Token bucket rate limiter
|
|
43
|
-
this._rateTokens = RATE_LIMIT_TOKENS;
|
|
44
|
-
this._rateLastRefill = Date.now();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if session has expired
|
|
49
|
-
* @returns {boolean}
|
|
50
|
-
*/
|
|
51
|
-
isExpired() {
|
|
52
|
-
return Date.now() - this.lastActivity.getTime() > SESSION_TIMEOUT_MS;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Rate-limit incoming messages (token bucket)
|
|
57
|
-
* @returns {boolean} true if allowed, false if rate-limited
|
|
58
|
-
*/
|
|
59
|
-
checkRateLimit() {
|
|
60
|
-
const now = Date.now();
|
|
61
|
-
const elapsed = now - this._rateLastRefill;
|
|
62
|
-
|
|
63
|
-
// Refill tokens based on elapsed time
|
|
64
|
-
if (elapsed >= RATE_LIMIT_REFILL_MS) {
|
|
65
|
-
this._rateTokens = RATE_LIMIT_TOKENS;
|
|
66
|
-
this._rateLastRefill = now;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (this._rateTokens <= 0) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this._rateTokens--;
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Send a message to the dashboard
|
|
79
|
-
* @param {Object} message - Message object
|
|
80
|
-
*/
|
|
81
|
-
send(message) {
|
|
82
|
-
if (this.ws && this.ws.writable) {
|
|
83
|
-
try {
|
|
84
|
-
const frame = encodeWebSocketFrame(getProtocol().serializeMessage(message));
|
|
85
|
-
this.ws.write(frame);
|
|
86
|
-
this.lastActivity = new Date();
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error(`[Session ${this.id}] Send error:`, error.message);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Add a message to conversation history
|
|
95
|
-
* @param {string} role - 'user' or 'assistant'
|
|
96
|
-
* @param {string} content - Message content
|
|
97
|
-
*/
|
|
98
|
-
addMessage(role, content) {
|
|
99
|
-
this.messages.push({
|
|
100
|
-
role,
|
|
101
|
-
content,
|
|
102
|
-
timestamp: new Date().toISOString(),
|
|
103
|
-
});
|
|
104
|
-
this.lastActivity = new Date();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get conversation history
|
|
109
|
-
* @returns {Array}
|
|
110
|
-
*/
|
|
111
|
-
getHistory() {
|
|
112
|
-
return this.messages;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Update session state
|
|
117
|
-
* @param {string} state - New state (connected, thinking, idle, error)
|
|
118
|
-
*/
|
|
119
|
-
setState(state) {
|
|
120
|
-
this.state = state;
|
|
121
|
-
this.send(
|
|
122
|
-
getProtocol().createSessionState(this.id, state, {
|
|
123
|
-
messageCount: this.messages.length,
|
|
124
|
-
lastActivity: this.lastActivity.toISOString(),
|
|
125
|
-
})
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
module.exports = {
|
|
131
|
-
DashboardSession,
|
|
132
|
-
SESSION_TIMEOUT_MS,
|
|
133
|
-
SESSION_CLEANUP_INTERVAL_MS,
|
|
134
|
-
RATE_LIMIT_TOKENS,
|
|
135
|
-
RATE_LIMIT_REFILL_MS,
|
|
136
|
-
};
|
package/lib/dashboard-status.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* dashboard-status.js - Status and Team Metrics
|
|
5
|
-
*
|
|
6
|
-
* Reads project status (stories/epics) and team metrics from disk,
|
|
7
|
-
* and formats them for dashboard display.
|
|
8
|
-
* Extracted from dashboard-server.js for testability.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Build a project status summary from status.json
|
|
16
|
-
* @param {string} projectRoot - Project root directory
|
|
17
|
-
* @returns {Object|null} - Status summary or null if unavailable
|
|
18
|
-
*/
|
|
19
|
-
function buildStatusSummary(projectRoot) {
|
|
20
|
-
const statusPath = path.join(projectRoot, 'docs', '09-agents', 'status.json');
|
|
21
|
-
if (!fs.existsSync(statusPath)) return null;
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
25
|
-
const stories = data.stories || {};
|
|
26
|
-
const epics = data.epics || {};
|
|
27
|
-
|
|
28
|
-
const storyValues = Object.values(stories);
|
|
29
|
-
return {
|
|
30
|
-
total: storyValues.length,
|
|
31
|
-
done: storyValues.filter(s => s.status === 'done' || s.status === 'completed').length,
|
|
32
|
-
inProgress: storyValues.filter(s => s.status === 'in-progress').length,
|
|
33
|
-
ready: storyValues.filter(s => s.status === 'ready').length,
|
|
34
|
-
blocked: storyValues.filter(s => s.status === 'blocked').length,
|
|
35
|
-
epics: Object.entries(epics).map(([id, e]) => ({
|
|
36
|
-
id,
|
|
37
|
-
title: e.title || id,
|
|
38
|
-
status: e.status || 'unknown',
|
|
39
|
-
storyCount: (e.stories || []).length,
|
|
40
|
-
doneCount: (e.stories || []).filter(
|
|
41
|
-
sid =>
|
|
42
|
-
stories[sid] && (stories[sid].status === 'done' || stories[sid].status === 'completed')
|
|
43
|
-
).length,
|
|
44
|
-
})),
|
|
45
|
-
};
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.error('[Status Update Error]', error.message);
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Read team metrics traces from session-state.json
|
|
54
|
-
* @param {string} projectRoot - Project root directory
|
|
55
|
-
* @returns {Object} - Map of traceId -> metrics, or empty object
|
|
56
|
-
*/
|
|
57
|
-
function readTeamMetrics(projectRoot) {
|
|
58
|
-
const sessionStatePath = path.join(projectRoot, 'docs', '09-agents', 'session-state.json');
|
|
59
|
-
if (!fs.existsSync(sessionStatePath)) return {};
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
63
|
-
return (state.team_metrics && state.team_metrics.traces) || {};
|
|
64
|
-
} catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = {
|
|
70
|
-
buildStatusSummary,
|
|
71
|
-
readTeamMetrics,
|
|
72
|
-
};
|
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* dashboard-terminal.js - Terminal Management
|
|
5
|
-
*
|
|
6
|
-
* Handles PTY-based terminal instances and manages multiple
|
|
7
|
-
* terminals per session. Supports node-pty with basic spawn fallback.
|
|
8
|
-
* Extracted from dashboard-server.js for testability.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// Lazy-loaded dependencies
|
|
12
|
-
let _childProcess, _crypto, _protocol;
|
|
13
|
-
|
|
14
|
-
function getChildProcess() {
|
|
15
|
-
if (!_childProcess) _childProcess = require('child_process');
|
|
16
|
-
return _childProcess;
|
|
17
|
-
}
|
|
18
|
-
function getCrypto() {
|
|
19
|
-
if (!_crypto) _crypto = require('crypto');
|
|
20
|
-
return _crypto;
|
|
21
|
-
}
|
|
22
|
-
function getProtocol() {
|
|
23
|
-
if (!_protocol) _protocol = require('./dashboard-protocol');
|
|
24
|
-
return _protocol;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Sensitive env var patterns to strip from terminal spawn
|
|
28
|
-
const SENSITIVE_ENV_PATTERNS = /SECRET|TOKEN|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY|AUTH/i;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Terminal instance for integrated terminal
|
|
32
|
-
*/
|
|
33
|
-
class TerminalInstance {
|
|
34
|
-
constructor(id, session, options = {}) {
|
|
35
|
-
this.id = id;
|
|
36
|
-
this.session = session;
|
|
37
|
-
this.cwd = options.cwd || session.projectRoot;
|
|
38
|
-
this.shell = options.shell || this.getDefaultShell();
|
|
39
|
-
this.cols = options.cols || 80;
|
|
40
|
-
this.rows = options.rows || 24;
|
|
41
|
-
this.pty = null;
|
|
42
|
-
this.closed = false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get the default shell for the current OS
|
|
47
|
-
*/
|
|
48
|
-
getDefaultShell() {
|
|
49
|
-
if (process.platform === 'win32') {
|
|
50
|
-
return process.env.COMSPEC || 'cmd.exe';
|
|
51
|
-
}
|
|
52
|
-
return process.env.SHELL || '/bin/bash';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get a filtered copy of environment variables with secrets removed
|
|
57
|
-
*/
|
|
58
|
-
_getFilteredEnv() {
|
|
59
|
-
const filtered = {};
|
|
60
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
61
|
-
if (!SENSITIVE_ENV_PATTERNS.test(key)) {
|
|
62
|
-
filtered[key] = value;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return filtered;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Start the terminal process
|
|
70
|
-
*/
|
|
71
|
-
start() {
|
|
72
|
-
try {
|
|
73
|
-
// Try to use node-pty for proper PTY support
|
|
74
|
-
const pty = require('node-pty');
|
|
75
|
-
const filteredEnv = this._getFilteredEnv();
|
|
76
|
-
|
|
77
|
-
this.pty = pty.spawn(this.shell, [], {
|
|
78
|
-
name: 'xterm-256color',
|
|
79
|
-
cols: this.cols,
|
|
80
|
-
rows: this.rows,
|
|
81
|
-
cwd: this.cwd,
|
|
82
|
-
env: {
|
|
83
|
-
...filteredEnv,
|
|
84
|
-
TERM: 'xterm-256color',
|
|
85
|
-
COLORTERM: 'truecolor',
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
this.pty.onData(data => {
|
|
90
|
-
if (!this.closed) {
|
|
91
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data));
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
this.pty.onExit(({ exitCode }) => {
|
|
96
|
-
this.closed = true;
|
|
97
|
-
this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
// Fallback to basic spawn if node-pty is not available
|
|
103
|
-
console.warn('[Terminal] node-pty not available, using basic spawn:', error.message);
|
|
104
|
-
return this.startBasicShell();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Fallback shell using basic spawn (no PTY)
|
|
110
|
-
* Note: This provides limited functionality without node-pty
|
|
111
|
-
*/
|
|
112
|
-
startBasicShell() {
|
|
113
|
-
try {
|
|
114
|
-
const filteredEnv = this._getFilteredEnv();
|
|
115
|
-
|
|
116
|
-
// Use bash with interactive flag for better compatibility
|
|
117
|
-
this.pty = getChildProcess().spawn(this.shell, ['-i'], {
|
|
118
|
-
cwd: this.cwd,
|
|
119
|
-
env: {
|
|
120
|
-
...filteredEnv,
|
|
121
|
-
TERM: 'dumb',
|
|
122
|
-
PS1: '\\w $ ', // Simple prompt
|
|
123
|
-
},
|
|
124
|
-
shell: false,
|
|
125
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Track input buffer for local echo (since no PTY)
|
|
129
|
-
this.inputBuffer = '';
|
|
130
|
-
|
|
131
|
-
this.pty.stdout.on('data', data => {
|
|
132
|
-
if (!this.closed) {
|
|
133
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
this.pty.stderr.on('data', data => {
|
|
138
|
-
if (!this.closed) {
|
|
139
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
this.pty.on('close', exitCode => {
|
|
144
|
-
this.closed = true;
|
|
145
|
-
this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
this.pty.on('error', error => {
|
|
149
|
-
console.error('[Terminal] Shell error:', error.message);
|
|
150
|
-
if (!this.closed) {
|
|
151
|
-
this.session.send(
|
|
152
|
-
getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`)
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Send welcome message
|
|
158
|
-
setTimeout(() => {
|
|
159
|
-
if (!this.closed) {
|
|
160
|
-
const welcomeMsg = `\x1b[32mAgileFlow Terminal\x1b[0m (basic mode - node-pty not available)\r\n`;
|
|
161
|
-
const cwdMsg = `Working directory: ${this.cwd}\r\n\r\n`;
|
|
162
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, welcomeMsg + cwdMsg));
|
|
163
|
-
}
|
|
164
|
-
}, 100);
|
|
165
|
-
|
|
166
|
-
return true;
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error('[Terminal] Failed to start basic shell:', error.message);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Write data to the terminal
|
|
175
|
-
* @param {string} data - Data to write (user input)
|
|
176
|
-
*/
|
|
177
|
-
write(data) {
|
|
178
|
-
if (this.pty && !this.closed) {
|
|
179
|
-
if (this.pty.write) {
|
|
180
|
-
// node-pty style - has built-in echo
|
|
181
|
-
this.pty.write(data);
|
|
182
|
-
} else if (this.pty.stdin) {
|
|
183
|
-
// basic spawn style - need manual echo
|
|
184
|
-
// Echo the input back to the terminal (since no PTY)
|
|
185
|
-
let echoData = data;
|
|
186
|
-
|
|
187
|
-
// Handle special characters
|
|
188
|
-
if (data === '\r' || data === '\n') {
|
|
189
|
-
echoData = '\r\n';
|
|
190
|
-
} else if (data === '\x7f' || data === '\b') {
|
|
191
|
-
// Backspace - move cursor back and clear
|
|
192
|
-
echoData = '\b \b';
|
|
193
|
-
} else if (data === '\x03') {
|
|
194
|
-
// Ctrl+C
|
|
195
|
-
echoData = '^C\r\n';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Echo to terminal
|
|
199
|
-
this.session.send(getProtocol().createTerminalOutput(this.id, echoData));
|
|
200
|
-
|
|
201
|
-
// Send to shell stdin
|
|
202
|
-
this.pty.stdin.write(data);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Resize the terminal
|
|
209
|
-
* @param {number} cols - New column count
|
|
210
|
-
* @param {number} rows - New row count
|
|
211
|
-
*/
|
|
212
|
-
resize(cols, rows) {
|
|
213
|
-
this.cols = cols;
|
|
214
|
-
this.rows = rows;
|
|
215
|
-
if (this.pty && this.pty.resize && !this.closed) {
|
|
216
|
-
this.pty.resize(cols, rows);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Close the terminal
|
|
222
|
-
*/
|
|
223
|
-
close() {
|
|
224
|
-
this.closed = true;
|
|
225
|
-
if (this.pty) {
|
|
226
|
-
if (this.pty.kill) {
|
|
227
|
-
this.pty.kill();
|
|
228
|
-
} else if (this.pty.destroy) {
|
|
229
|
-
this.pty.destroy();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Terminal manager for handling multiple terminals per session
|
|
237
|
-
*/
|
|
238
|
-
class TerminalManager {
|
|
239
|
-
constructor() {
|
|
240
|
-
this.terminals = new Map();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Create a new terminal for a session
|
|
245
|
-
* @param {DashboardSession} session - The session
|
|
246
|
-
* @param {Object} options - Terminal options
|
|
247
|
-
* @returns {string} - Terminal ID
|
|
248
|
-
*/
|
|
249
|
-
createTerminal(session, options = {}) {
|
|
250
|
-
const terminalId = options.id || getCrypto().randomBytes(8).toString('hex');
|
|
251
|
-
const terminal = new TerminalInstance(terminalId, session, {
|
|
252
|
-
cwd: options.cwd || session.projectRoot,
|
|
253
|
-
cols: options.cols,
|
|
254
|
-
rows: options.rows,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
if (terminal.start()) {
|
|
258
|
-
this.terminals.set(terminalId, terminal);
|
|
259
|
-
console.log(`[Terminal ${terminalId}] Created for session ${session.id}`);
|
|
260
|
-
return terminalId;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get a terminal by ID
|
|
268
|
-
* @param {string} terminalId - Terminal ID
|
|
269
|
-
* @returns {TerminalInstance | undefined}
|
|
270
|
-
*/
|
|
271
|
-
getTerminal(terminalId) {
|
|
272
|
-
return this.terminals.get(terminalId);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Write to a terminal
|
|
277
|
-
* @param {string} terminalId - Terminal ID
|
|
278
|
-
* @param {string} data - Data to write
|
|
279
|
-
*/
|
|
280
|
-
writeToTerminal(terminalId, data) {
|
|
281
|
-
const terminal = this.terminals.get(terminalId);
|
|
282
|
-
if (terminal) {
|
|
283
|
-
terminal.write(data);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Resize a terminal
|
|
289
|
-
* @param {string} terminalId - Terminal ID
|
|
290
|
-
* @param {number} cols - New columns
|
|
291
|
-
* @param {number} rows - New rows
|
|
292
|
-
*/
|
|
293
|
-
resizeTerminal(terminalId, cols, rows) {
|
|
294
|
-
const terminal = this.terminals.get(terminalId);
|
|
295
|
-
if (terminal) {
|
|
296
|
-
terminal.resize(cols, rows);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Close a terminal
|
|
302
|
-
* @param {string} terminalId - Terminal ID
|
|
303
|
-
*/
|
|
304
|
-
closeTerminal(terminalId) {
|
|
305
|
-
const terminal = this.terminals.get(terminalId);
|
|
306
|
-
if (terminal) {
|
|
307
|
-
terminal.close();
|
|
308
|
-
this.terminals.delete(terminalId);
|
|
309
|
-
console.log(`[Terminal ${terminalId}] Closed`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Close all terminals for a session
|
|
315
|
-
* @param {string} sessionId - Session ID
|
|
316
|
-
*/
|
|
317
|
-
closeSessionTerminals(sessionId) {
|
|
318
|
-
// Collect IDs first to avoid mutating Map during iteration
|
|
319
|
-
const toClose = [];
|
|
320
|
-
for (const [terminalId, terminal] of this.terminals) {
|
|
321
|
-
if (terminal.session.id === sessionId) {
|
|
322
|
-
toClose.push(terminalId);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
for (const terminalId of toClose) {
|
|
326
|
-
const terminal = this.terminals.get(terminalId);
|
|
327
|
-
if (terminal) {
|
|
328
|
-
terminal.close();
|
|
329
|
-
this.terminals.delete(terminalId);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Get all terminals for a session
|
|
336
|
-
* @param {string} sessionId - Session ID
|
|
337
|
-
* @returns {Array<string>} - Terminal IDs
|
|
338
|
-
*/
|
|
339
|
-
getSessionTerminals(sessionId) {
|
|
340
|
-
const terminalIds = [];
|
|
341
|
-
for (const [terminalId, terminal] of this.terminals) {
|
|
342
|
-
if (terminal.session.id === sessionId) {
|
|
343
|
-
terminalIds.push(terminalId);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return terminalIds;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
module.exports = {
|
|
351
|
-
TerminalInstance,
|
|
352
|
-
TerminalManager,
|
|
353
|
-
SENSITIVE_ENV_PATTERNS,
|
|
354
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* dashboard-websocket.js - WebSocket Frame Encoding/Decoding
|
|
5
|
-
*
|
|
6
|
-
* Pure functions for encoding and decoding WebSocket frames.
|
|
7
|
-
* Extracted from dashboard-server.js for testability.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Encode a WebSocket frame
|
|
12
|
-
* @param {string|Buffer} data - Data to encode
|
|
13
|
-
* @param {number} [opcode=0x1] - Frame opcode (0x1 = text, 0x2 = binary)
|
|
14
|
-
* @returns {Buffer}
|
|
15
|
-
*/
|
|
16
|
-
function encodeWebSocketFrame(data, opcode = 0x1) {
|
|
17
|
-
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
18
|
-
const length = payload.length;
|
|
19
|
-
|
|
20
|
-
let header;
|
|
21
|
-
if (length < 126) {
|
|
22
|
-
header = Buffer.alloc(2);
|
|
23
|
-
header[0] = 0x80 | opcode; // FIN + opcode
|
|
24
|
-
header[1] = length;
|
|
25
|
-
} else if (length < 65536) {
|
|
26
|
-
header = Buffer.alloc(4);
|
|
27
|
-
header[0] = 0x80 | opcode;
|
|
28
|
-
header[1] = 126;
|
|
29
|
-
header.writeUInt16BE(length, 2);
|
|
30
|
-
} else {
|
|
31
|
-
header = Buffer.alloc(10);
|
|
32
|
-
header[0] = 0x80 | opcode;
|
|
33
|
-
header[1] = 127;
|
|
34
|
-
header.writeBigUInt64BE(BigInt(length), 2);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return Buffer.concat([header, payload]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Decode a WebSocket frame
|
|
42
|
-
* @param {Buffer} buffer - Buffer containing frame data
|
|
43
|
-
* @returns {{ opcode: number, payload: Buffer, totalLength: number } | null}
|
|
44
|
-
*/
|
|
45
|
-
function decodeWebSocketFrame(buffer) {
|
|
46
|
-
if (buffer.length < 2) return null;
|
|
47
|
-
|
|
48
|
-
const firstByte = buffer[0];
|
|
49
|
-
const secondByte = buffer[1];
|
|
50
|
-
|
|
51
|
-
const opcode = firstByte & 0x0f;
|
|
52
|
-
const masked = (secondByte & 0x80) !== 0;
|
|
53
|
-
let payloadLength = secondByte & 0x7f;
|
|
54
|
-
|
|
55
|
-
let headerLength = 2;
|
|
56
|
-
if (payloadLength === 126) {
|
|
57
|
-
if (buffer.length < 4) return null;
|
|
58
|
-
payloadLength = buffer.readUInt16BE(2);
|
|
59
|
-
headerLength = 4;
|
|
60
|
-
} else if (payloadLength === 127) {
|
|
61
|
-
if (buffer.length < 10) return null;
|
|
62
|
-
payloadLength = Number(buffer.readBigUInt64BE(2));
|
|
63
|
-
headerLength = 10;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (masked) headerLength += 4;
|
|
67
|
-
|
|
68
|
-
const totalLength = headerLength + payloadLength;
|
|
69
|
-
if (buffer.length < totalLength) return null;
|
|
70
|
-
|
|
71
|
-
let payload = buffer.slice(headerLength, totalLength);
|
|
72
|
-
|
|
73
|
-
// Unmask if needed
|
|
74
|
-
if (masked) {
|
|
75
|
-
const mask = buffer.slice(headerLength - 4, headerLength);
|
|
76
|
-
payload = Buffer.alloc(payloadLength);
|
|
77
|
-
for (let i = 0; i < payloadLength; i++) {
|
|
78
|
-
payload[i] = buffer[headerLength + i] ^ mask[i % 4];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return { opcode, payload, totalLength };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
module.exports = {
|
|
86
|
-
encodeWebSocketFrame,
|
|
87
|
-
decodeWebSocketFrame,
|
|
88
|
-
};
|