glad-web 1.0.45 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -192
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/bin/glad.cjs +56 -0
- package/package.json +19 -58
- package/README.zh-CN.md +0 -198
- package/assets/logo.svg +0 -43
- package/bin/cli.js +0 -65
- package/lib/ai-tools/demo/enhanced-demo.js +0 -625
- package/lib/ai-tools/demo/index.js +0 -24
- package/lib/ai-tools/demo/responses.js +0 -88
- package/lib/ai-tools/detector.js +0 -76
- package/lib/ai-tools/registry.js +0 -300
- package/lib/claude/cli-usage.js +0 -95
- package/lib/claude/config.js +0 -82
- package/lib/claude/structured-session.js +0 -884
- package/lib/claude/transcript-repository.js +0 -216
- package/lib/codex/image-store.js +0 -174
- package/lib/codex/structured-session.js +0 -1578
- package/lib/commands/config.js +0 -78
- package/lib/commands/tools.js +0 -128
- package/lib/commands/web.js +0 -586
- package/lib/config/constants.js +0 -17
- package/lib/config/manager.js +0 -89
- package/lib/git/service.js +0 -83
- package/lib/notifications/message-formatter.js +0 -94
- package/lib/notifications/notification-service.js +0 -143
- package/lib/notifications/serverchan-client.js +0 -58
- package/lib/notifications/serverchan-settings-store.js +0 -115
- package/lib/schedule/job-runner.js +0 -162
- package/lib/schedule/job-store.js +0 -167
- package/lib/schedule/key-sequences.js +0 -49
- package/lib/schedule/scheduler-service.js +0 -39
- package/lib/server/routes/notifications.js +0 -52
- package/lib/server/routes/providers.js +0 -114
- package/lib/server/routes/schedules.js +0 -54
- package/lib/server/routes/usage.js +0 -23
- package/lib/server/routes/workspace.js +0 -77
- package/lib/session/buffer.js +0 -102
- package/lib/session/file-attachment-store.js +0 -168
- package/lib/session/pty-manager.js +0 -255
- package/lib/session/rendered-history.js +0 -225
- package/lib/session/session-manager.js +0 -1001
- package/lib/session/text-history.js +0 -274
- package/lib/usage/ccusage-runner.js +0 -128
- package/lib/usage/source-catalog.js +0 -26
- package/lib/usage/usage-service.js +0 -226
- package/lib/utils/logger.js +0 -74
- package/lib/utils/pid.js +0 -67
- package/lib/utils/validation.js +0 -53
- package/lib/web/claude.js +0 -1129
- package/lib/web/codex.js +0 -1042
- package/lib/web/composer.js +0 -463
- package/lib/web/core.js +0 -373
- package/lib/web/git.js +0 -535
- package/lib/web/gitgraph.js +0 -293
- package/lib/web/index.html +0 -516
- package/lib/web/layout.js +0 -72
- package/lib/web/notifications.js +0 -163
- package/lib/web/schedules.js +0 -245
- package/lib/web/session.js +0 -360
- package/lib/web/shell.js +0 -59
- package/lib/web/styles.css +0 -905
- package/lib/web/terminal-scroll.js +0 -81
- package/lib/web/theme.js +0 -60
- package/lib/web/timed-inputs.js +0 -216
- package/lib/web/usage.js +0 -323
- package/lib/workspace/service.js +0 -77
- package/scripts/check-syntax.js +0 -26
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { v4: uuidv4 } = require('uuid');
|
|
5
|
-
|
|
6
|
-
const MAX_BYTES = 50 * 1024 * 1024;
|
|
7
|
-
const MAX_PER_SESSION = 8;
|
|
8
|
-
const MAX_CHUNKS = 128;
|
|
9
|
-
const CLEANUP_DELAY_MS = 30 * 60 * 1000;
|
|
10
|
-
|
|
11
|
-
function inputError(message, statusCode = 400) {
|
|
12
|
-
const error = new Error(message);
|
|
13
|
-
error.statusCode = statusCode;
|
|
14
|
-
return error;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function safeUploadId(value) {
|
|
18
|
-
return typeof value === 'string' && /^[a-zA-Z0-9-]{8,100}$/.test(value);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function safeFileName(value) {
|
|
22
|
-
let decoded = String(value || 'attachment.bin');
|
|
23
|
-
try { decoded = decodeURIComponent(decoded); } catch (_) {}
|
|
24
|
-
const base = decoded.replace(/\\/g, '/').split('/').pop() || 'attachment.bin';
|
|
25
|
-
const cleaned = base.replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_').trim().slice(0, 160);
|
|
26
|
-
return cleaned || 'attachment.bin';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class FileAttachmentStore {
|
|
30
|
-
constructor({ logger, root, uploadRoot } = {}) {
|
|
31
|
-
this.logger = logger || console;
|
|
32
|
-
this.root = root || path.join(os.tmpdir(), 'glad', 'session-files');
|
|
33
|
-
this.uploadRoot = uploadRoot || path.join(os.tmpdir(), 'glad', 'session-file-uploads');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
assertSession(session) {
|
|
37
|
-
if (!session) throw inputError('Session not found', 404);
|
|
38
|
-
if (!session.fileAttachments || !session.fileUploads) throw inputError('File attachments are unavailable for this session');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async appendChunk(session, input = {}, bytes) {
|
|
42
|
-
this.assertSession(session);
|
|
43
|
-
if (!Buffer.isBuffer(bytes) || bytes.length === 0) throw inputError('File chunk is required');
|
|
44
|
-
const uploadId = String(input.uploadId || '');
|
|
45
|
-
const chunkIndex = Number(input.chunkIndex);
|
|
46
|
-
const chunkTotal = Number(input.chunkTotal);
|
|
47
|
-
const name = safeFileName(input.name);
|
|
48
|
-
if (!safeUploadId(uploadId)) throw inputError('Invalid file upload id');
|
|
49
|
-
if (!Number.isInteger(chunkIndex) || !Number.isInteger(chunkTotal) || chunkIndex < 0 || chunkTotal < 1 || chunkTotal > MAX_CHUNKS || chunkIndex >= chunkTotal) {
|
|
50
|
-
throw inputError('Invalid file chunk metadata');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let upload = session.fileUploads.get(uploadId);
|
|
54
|
-
if (!upload) {
|
|
55
|
-
if (chunkIndex !== 0) throw inputError('File upload must start with the first chunk');
|
|
56
|
-
const directory = path.join(this.uploadRoot, session.id);
|
|
57
|
-
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
58
|
-
upload = { id: uploadId, name, path: path.join(directory, `${uploadId}.part`), chunkTotal, nextChunkIndex: 0, bytes: 0 };
|
|
59
|
-
session.fileUploads.set(uploadId, upload);
|
|
60
|
-
}
|
|
61
|
-
if (upload.name !== name || upload.chunkTotal !== chunkTotal || upload.nextChunkIndex !== chunkIndex) {
|
|
62
|
-
throw inputError('File chunks arrived out of order');
|
|
63
|
-
}
|
|
64
|
-
if (upload.bytes + bytes.length > MAX_BYTES) {
|
|
65
|
-
await this.discardUpload(session, uploadId);
|
|
66
|
-
throw inputError('File must be 50 MB or smaller');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (chunkIndex === 0) await fs.promises.writeFile(upload.path, bytes, { mode: 0o600, flag: 'wx' });
|
|
70
|
-
else await fs.promises.appendFile(upload.path, bytes, { mode: 0o600 });
|
|
71
|
-
upload.bytes += bytes.length;
|
|
72
|
-
upload.nextChunkIndex += 1;
|
|
73
|
-
if (upload.nextChunkIndex < upload.chunkTotal) {
|
|
74
|
-
return { complete: false, receivedChunks: upload.nextChunkIndex, size: upload.bytes };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const pendingAttachmentCount = Array.from(session.fileAttachments.values()).filter(item => !item.sent).length;
|
|
78
|
-
if (pendingAttachmentCount >= MAX_PER_SESSION) {
|
|
79
|
-
await this.discardUpload(session, uploadId);
|
|
80
|
-
throw inputError(`You can attach at most ${MAX_PER_SESSION} files at a time`);
|
|
81
|
-
}
|
|
82
|
-
const directory = path.join(this.root, session.id);
|
|
83
|
-
await fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
84
|
-
const attachment = {
|
|
85
|
-
id: uuidv4(),
|
|
86
|
-
name: upload.name,
|
|
87
|
-
path: path.join(directory, `${uuidv4()}-${upload.name}`),
|
|
88
|
-
size: upload.bytes,
|
|
89
|
-
createdAt: Date.now(),
|
|
90
|
-
cleanupTimer: null
|
|
91
|
-
};
|
|
92
|
-
session.fileUploads.delete(uploadId);
|
|
93
|
-
try {
|
|
94
|
-
await fs.promises.rename(upload.path, attachment.path);
|
|
95
|
-
await fs.promises.chmod(attachment.path, 0o600);
|
|
96
|
-
session.fileAttachments.set(attachment.id, attachment);
|
|
97
|
-
return { complete: true, attachment: { id: attachment.id, name: attachment.name, size: attachment.size, kind: 'file' } };
|
|
98
|
-
} catch (error) {
|
|
99
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async discardUpload(session, uploadId) {
|
|
105
|
-
if (!session?.fileUploads || !safeUploadId(uploadId)) return false;
|
|
106
|
-
const upload = session.fileUploads.get(uploadId);
|
|
107
|
-
if (!upload) return false;
|
|
108
|
-
session.fileUploads.delete(uploadId);
|
|
109
|
-
await fs.promises.rm(upload.path, { force: true });
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async discardAttachment(session, attachmentId) {
|
|
114
|
-
if (!session?.fileAttachments) return false;
|
|
115
|
-
const attachment = session.fileAttachments.get(attachmentId);
|
|
116
|
-
if (!attachment) return false;
|
|
117
|
-
clearTimeout(attachment.cleanupTimer);
|
|
118
|
-
session.fileAttachments.delete(attachmentId);
|
|
119
|
-
await fs.promises.rm(attachment.path, { force: true });
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
resolve(session, attachmentIds = []) {
|
|
124
|
-
const ids = Array.isArray(attachmentIds) ? attachmentIds : [];
|
|
125
|
-
if (!session) throw inputError('Session not found', 404);
|
|
126
|
-
if (ids.length === 0) return [];
|
|
127
|
-
this.assertSession(session);
|
|
128
|
-
if (ids.length > MAX_PER_SESSION) throw inputError(`You can attach at most ${MAX_PER_SESSION} files at a time`);
|
|
129
|
-
const uniqueIds = [...new Set(ids.map(String))];
|
|
130
|
-
if (uniqueIds.length !== ids.length) throw inputError('Duplicate file attachment');
|
|
131
|
-
return uniqueIds.map(attachmentId => {
|
|
132
|
-
const attachment = session.fileAttachments.get(attachmentId);
|
|
133
|
-
if (!attachment) throw inputError('File attachment is no longer available');
|
|
134
|
-
return attachment;
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
scheduleCleanup(session, attachmentIds) {
|
|
139
|
-
if (!session?.fileAttachments) return;
|
|
140
|
-
for (const attachmentId of attachmentIds) {
|
|
141
|
-
const attachment = session.fileAttachments.get(attachmentId);
|
|
142
|
-
if (!attachment) continue;
|
|
143
|
-
attachment.sent = true;
|
|
144
|
-
clearTimeout(attachment.cleanupTimer);
|
|
145
|
-
attachment.cleanupTimer = setTimeout(() => {
|
|
146
|
-
this.discardAttachment(session, attachmentId).catch(error => {
|
|
147
|
-
this.logger.debugInfo?.(`[file-attachment] cleanup failed: ${error.message}`);
|
|
148
|
-
});
|
|
149
|
-
}, CLEANUP_DELAY_MS);
|
|
150
|
-
attachment.cleanupTimer.unref?.();
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
clear(session) {
|
|
155
|
-
if (!session) return;
|
|
156
|
-
for (const attachment of session.fileAttachments?.values?.() || []) clearTimeout(attachment.cleanupTimer);
|
|
157
|
-
session.fileAttachments?.clear?.();
|
|
158
|
-
session.fileUploads?.clear?.();
|
|
159
|
-
fs.promises.rm(path.join(this.root, session.id), { recursive: true, force: true }).catch(error => {
|
|
160
|
-
this.logger.debugInfo?.(`[file-attachment] cleanup failed: ${error.message}`);
|
|
161
|
-
});
|
|
162
|
-
fs.promises.rm(path.join(this.uploadRoot, session.id), { recursive: true, force: true }).catch(error => {
|
|
163
|
-
this.logger.debugInfo?.(`[file-upload] cleanup failed: ${error.message}`);
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
module.exports = FileAttachmentStore;
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
const pty = require('node-pty');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const logger = require('../utils/logger');
|
|
5
|
-
|
|
6
|
-
function previewText(text, maxChars = 500) {
|
|
7
|
-
const normalized = String(text || '')
|
|
8
|
-
.replace(/\r/g, '\\r')
|
|
9
|
-
.replace(/\n/g, '\\n')
|
|
10
|
-
.replace(/\t/g, '\\t')
|
|
11
|
-
.replace(/\x1b/g, '\\x1b');
|
|
12
|
-
return normalized.length > maxChars ? normalized.slice(0, maxChars) + '...' : normalized;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class PTYManager {
|
|
16
|
-
constructor(tool, workingDir, buffer, options = {}) {
|
|
17
|
-
this.tool = tool;
|
|
18
|
-
this.workingDir = workingDir;
|
|
19
|
-
this.buffer = buffer;
|
|
20
|
-
this.silent = options.silent || false;
|
|
21
|
-
this.ptyProcess = null;
|
|
22
|
-
this.onDataCallback = null;
|
|
23
|
-
this.onExitCallback = null;
|
|
24
|
-
this.localResizeListener = null;
|
|
25
|
-
|
|
26
|
-
this.tuiTools = ['antigravity', 'opencode', 'kilo'];
|
|
27
|
-
this.isTUIMode = this.tuiTools.includes(tool.key);
|
|
28
|
-
|
|
29
|
-
this.recentOutputs = [];
|
|
30
|
-
this.duplicateThresholdMs = 150;
|
|
31
|
-
this.maxRecentOutputs = 10;
|
|
32
|
-
this.debugOutputCount = 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Start PTY process
|
|
36
|
-
start(additionalArgs = []) {
|
|
37
|
-
const isWindows = os.platform() === 'win32';
|
|
38
|
-
const args = [...this.tool.args, ...additionalArgs];
|
|
39
|
-
|
|
40
|
-
logger.info(`Starting ${this.tool.displayName}...`);
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
if (!this.silent) {
|
|
44
|
-
process.stdout.write('\x1b[?2004l');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let spawnCommand = this.tool.command;
|
|
48
|
-
let spawnArgs = args;
|
|
49
|
-
|
|
50
|
-
if (isWindows) {
|
|
51
|
-
spawnCommand = 'cmd.exe';
|
|
52
|
-
spawnArgs = ['/c', this.tool.command, ...args];
|
|
53
|
-
} else {
|
|
54
|
-
spawnCommand = 'bash';
|
|
55
|
-
const escaped = [this.tool.command, ...args]
|
|
56
|
-
.map(a => `'${String(a).replace(/'/g, "'\\''")}'`)
|
|
57
|
-
.join(' ');
|
|
58
|
-
spawnArgs = ['-i', '-c', escaped];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let spawnEnv = {
|
|
62
|
-
...process.env,
|
|
63
|
-
TERM: 'xterm-256color',
|
|
64
|
-
COLORTERM: 'truecolor',
|
|
65
|
-
FORCE_COLOR: '1',
|
|
66
|
-
CLICOLOR: '1'
|
|
67
|
-
};
|
|
68
|
-
delete spawnEnv.NO_COLOR;
|
|
69
|
-
delete spawnEnv.STY;
|
|
70
|
-
delete spawnEnv.WINDOW;
|
|
71
|
-
delete spawnEnv.TERMCAP;
|
|
72
|
-
|
|
73
|
-
if (isWindows) {
|
|
74
|
-
const path = require('path');
|
|
75
|
-
const npmGlobalBin = path.join(process.env.APPDATA || '', 'npm');
|
|
76
|
-
const currentPath = process.env.PATH || '';
|
|
77
|
-
|
|
78
|
-
spawnEnv = {
|
|
79
|
-
...spawnEnv,
|
|
80
|
-
PATH: `${npmGlobalBin};${currentPath}`
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
logger.debugInfo(`[pty-start] tool=${this.tool.key}; displayName=${this.tool.displayName}; command=${spawnCommand}; args=${JSON.stringify(spawnArgs)}; cwd=${this.workingDir}`);
|
|
85
|
-
logger.debugInfo(`[pty-start] PATH=${spawnEnv.PATH || ''}; SHELL=${spawnEnv.SHELL || ''}; TERM=${spawnEnv.TERM || ''}; HOME=${spawnEnv.HOME || ''}`);
|
|
86
|
-
|
|
87
|
-
this.ptyProcess = pty.spawn(spawnCommand, spawnArgs, {
|
|
88
|
-
name: 'xterm-256color',
|
|
89
|
-
cols: this.silent ? 80 : (process.stdout.columns || 80),
|
|
90
|
-
rows: this.silent ? 24 : (process.stdout.rows || 24),
|
|
91
|
-
cwd: this.workingDir,
|
|
92
|
-
env: spawnEnv
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
logger.success(`${this.tool.displayName} started (PID: ${this.ptyProcess.pid})`);
|
|
96
|
-
|
|
97
|
-
this.ptyProcess.onData((data) => {
|
|
98
|
-
this.handlePTYOutput(data);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
this.ptyProcess.onExit((exitCode) => {
|
|
102
|
-
this.handlePTYExit(exitCode);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (!this.silent && process.stdin.isTTY) {
|
|
106
|
-
process.stdin.setRawMode(true);
|
|
107
|
-
process.stdin.setEncoding('utf8');
|
|
108
|
-
process.stdin.resume();
|
|
109
|
-
|
|
110
|
-
process.stdin.on('data', (data) => {
|
|
111
|
-
const filtered = data.toString()
|
|
112
|
-
.replace(/\x1b\[I/g, '')
|
|
113
|
-
.replace(/\x1b\[O/g, '');
|
|
114
|
-
|
|
115
|
-
if (filtered.length > 0) {
|
|
116
|
-
this.ptyProcess.write(filtered);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (data === '\u0003') {
|
|
120
|
-
this.kill();
|
|
121
|
-
process.exit(0);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!this.silent) {
|
|
127
|
-
this.setupLocalResizeListener();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return true;
|
|
131
|
-
} catch (err) {
|
|
132
|
-
logger.error(`Failed to start ${this.tool.displayName}: ${err.message}`);
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Handle PTY output
|
|
138
|
-
handlePTYOutput(data) {
|
|
139
|
-
let filtered = data
|
|
140
|
-
.replace(/\x1b\[I/g, '')
|
|
141
|
-
.replace(/\x1b\[O/g, '');
|
|
142
|
-
|
|
143
|
-
if (filtered.length > 0) {
|
|
144
|
-
if (this.debugOutputCount < 12) {
|
|
145
|
-
this.debugOutputCount += 1;
|
|
146
|
-
logger.debugInfo(`[pty-output] tool=${this.tool.key}; chunk=${this.debugOutputCount}; bytes=${Buffer.byteLength(filtered, 'utf8')}; preview="${previewText(filtered)}"`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (os.platform() === 'win32') {
|
|
150
|
-
const now = Date.now();
|
|
151
|
-
const hash = crypto.createHash('sha256').update(filtered).digest('hex');
|
|
152
|
-
this.recentOutputs = this.recentOutputs.filter(entry => now - entry.timestamp < this.duplicateThresholdMs);
|
|
153
|
-
const duplicate = this.recentOutputs.find(entry => entry.hash === hash);
|
|
154
|
-
if (duplicate) return;
|
|
155
|
-
this.recentOutputs.push({ hash, timestamp: now });
|
|
156
|
-
if (this.recentOutputs.length > this.maxRecentOutputs) this.recentOutputs.shift();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!this.silent) {
|
|
160
|
-
process.stdout.write(filtered);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let forMobile = filtered;
|
|
164
|
-
if (os.platform() === 'win32' && forMobile.startsWith('\x1b[H\x1b[K')) {
|
|
165
|
-
forMobile = '\x1b[2J\x1b[3J\x1b[H' + forMobile.slice(6);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!this.isTUIMode) {
|
|
169
|
-
this.buffer.append(forMobile);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (this.onDataCallback) {
|
|
173
|
-
this.onDataCallback(forMobile);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
handlePTYExit(exitCode) {
|
|
179
|
-
logger.info(`${this.tool.displayName} exited with code ${exitCode.exitCode}`);
|
|
180
|
-
logger.debugInfo(`[pty-exit] tool=${this.tool.key}; exitCode=${exitCode.exitCode}; signal=${exitCode.signal || ''}`);
|
|
181
|
-
if (this.onExitCallback) {
|
|
182
|
-
this.onExitCallback(exitCode);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
write(data) {
|
|
187
|
-
if (this.ptyProcess) {
|
|
188
|
-
this.ptyProcess.write(data);
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
resize(cols, rows) {
|
|
195
|
-
if (this.ptyProcess) {
|
|
196
|
-
this.ptyProcess.resize(cols, rows);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
redraw(cols, rows) {
|
|
201
|
-
if (!this.ptyProcess) return false;
|
|
202
|
-
|
|
203
|
-
const targetCols = Math.max(2, Number.parseInt(cols, 10) || 80);
|
|
204
|
-
const targetRows = Math.max(1, Number.parseInt(rows, 10) || 24);
|
|
205
|
-
const pulseCols = targetCols > 2 ? targetCols - 1 : targetCols + 1;
|
|
206
|
-
|
|
207
|
-
this.ptyProcess.resize(pulseCols, targetRows);
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
if (this.ptyProcess) this.ptyProcess.resize(targetCols, targetRows);
|
|
210
|
-
}, 30);
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
setupLocalResizeListener() {
|
|
215
|
-
if (!process.stdout.isTTY) return;
|
|
216
|
-
this.localResizeListener = () => {
|
|
217
|
-
const cols = process.stdout.columns || 80;
|
|
218
|
-
const rows = process.stdout.rows || 24;
|
|
219
|
-
this.resize(cols, rows);
|
|
220
|
-
};
|
|
221
|
-
process.stdout.on('resize', this.localResizeListener);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
onData(callback) {
|
|
225
|
-
this.onDataCallback = callback;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
onExit(callback) {
|
|
229
|
-
this.onExitCallback = callback;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
kill() {
|
|
233
|
-
if (this.ptyProcess) {
|
|
234
|
-
this.ptyProcess.kill();
|
|
235
|
-
this.ptyProcess = null;
|
|
236
|
-
}
|
|
237
|
-
if (this.localResizeListener && process.stdout.off) {
|
|
238
|
-
process.stdout.off('resize', this.localResizeListener);
|
|
239
|
-
this.localResizeListener = null;
|
|
240
|
-
}
|
|
241
|
-
if (!this.silent && process.stdin.isTTY && process.stdin.setRawMode) {
|
|
242
|
-
process.stdin.setRawMode(false);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
isRunning() {
|
|
247
|
-
return this.ptyProcess !== null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
getPid() {
|
|
251
|
-
return this.ptyProcess ? this.ptyProcess.pid : null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
module.exports = PTYManager;
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
let TerminalCtor = null;
|
|
2
|
-
|
|
3
|
-
function getTerminalCtor() {
|
|
4
|
-
if (TerminalCtor) return TerminalCtor;
|
|
5
|
-
if (typeof global.window === 'undefined') {
|
|
6
|
-
global.window = {};
|
|
7
|
-
}
|
|
8
|
-
({ Terminal: TerminalCtor } = require('@xterm/headless'));
|
|
9
|
-
return TerminalCtor;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class RenderedHistory {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
const Terminal = getTerminalCtor();
|
|
15
|
-
|
|
16
|
-
this.maxBytes = options.maxBytes || 5 * 1024 * 1024;
|
|
17
|
-
this.debugLabel = options.debugLabel || 'session';
|
|
18
|
-
this.cols = Math.max(2, options.cols || 80);
|
|
19
|
-
this.rows = Math.max(1, options.rows || 24);
|
|
20
|
-
this.minCols = Math.max(2, options.minCols || 20);
|
|
21
|
-
this.minRows = Math.max(1, options.minRows || 8);
|
|
22
|
-
this.updatedAt = Date.now();
|
|
23
|
-
this.totalWrites = 0;
|
|
24
|
-
this.totalBytes = 0;
|
|
25
|
-
this.pendingWrites = 0;
|
|
26
|
-
this.resizeEvents = 0;
|
|
27
|
-
this.truncated = false;
|
|
28
|
-
this.lastEvents = [];
|
|
29
|
-
this.archivedLines = [];
|
|
30
|
-
this.archivedBytes = 0;
|
|
31
|
-
this.term = new Terminal({
|
|
32
|
-
cols: this.cols,
|
|
33
|
-
rows: this.rows,
|
|
34
|
-
scrollback: Math.max(20000, options.scrollback || 20000),
|
|
35
|
-
allowProposedApi: true
|
|
36
|
-
});
|
|
37
|
-
this.hookBufferTrim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
write(data) {
|
|
41
|
-
if (!this.term) return;
|
|
42
|
-
if (!data) return;
|
|
43
|
-
const text = String(data);
|
|
44
|
-
this.totalWrites += 1;
|
|
45
|
-
this.totalBytes += Buffer.byteLength(text, 'utf8');
|
|
46
|
-
this.pendingWrites += 1;
|
|
47
|
-
this.updatedAt = Date.now();
|
|
48
|
-
|
|
49
|
-
this.term.write(text, () => {
|
|
50
|
-
this.pendingWrites = Math.max(0, this.pendingWrites - 1);
|
|
51
|
-
this.updatedAt = Date.now();
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
resize(cols, rows) {
|
|
56
|
-
if (!this.term) return;
|
|
57
|
-
const rawCols = Number.parseInt(cols, 10);
|
|
58
|
-
const rawRows = Number.parseInt(rows, 10);
|
|
59
|
-
const nextCols = Math.max(2, rawCols || this.cols);
|
|
60
|
-
const nextRows = Math.max(1, rawRows || this.rows);
|
|
61
|
-
|
|
62
|
-
// Hidden or unstable layouts can briefly report pathological sizes like 10x6.
|
|
63
|
-
// Ignore those for rendered history so we don't reflow the whole buffer into noise.
|
|
64
|
-
if (nextCols < this.minCols || nextRows < this.minRows) {
|
|
65
|
-
this.recordEvent(`resize ignored ${nextCols}x${nextRows}`);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (nextCols === this.cols && nextRows === this.rows) return;
|
|
70
|
-
|
|
71
|
-
this.cols = nextCols;
|
|
72
|
-
this.rows = nextRows;
|
|
73
|
-
this.resizeEvents += 1;
|
|
74
|
-
this.updatedAt = Date.now();
|
|
75
|
-
this.recordEvent(`resize ${this.cols}x${this.rows}`);
|
|
76
|
-
this.term.resize(this.cols, this.rows);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
toJSON() {
|
|
80
|
-
const snapshot = this.buildSnapshot();
|
|
81
|
-
return {
|
|
82
|
-
text: snapshot.text,
|
|
83
|
-
updatedAt: this.updatedAt,
|
|
84
|
-
truncated: snapshot.truncated,
|
|
85
|
-
bytes: snapshot.bytes,
|
|
86
|
-
lines: snapshot.lines.length
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getDebugSnapshot(options = {}) {
|
|
91
|
-
const tailLines = options.tailLines || 12;
|
|
92
|
-
const snapshot = this.buildSnapshot();
|
|
93
|
-
const active = this.term.buffer.active;
|
|
94
|
-
return {
|
|
95
|
-
label: this.debugLabel,
|
|
96
|
-
updatedAt: this.updatedAt,
|
|
97
|
-
truncated: snapshot.truncated,
|
|
98
|
-
cols: this.cols,
|
|
99
|
-
rows: this.rows,
|
|
100
|
-
totalWrites: this.totalWrites,
|
|
101
|
-
totalBytes: this.totalBytes,
|
|
102
|
-
pendingWrites: this.pendingWrites,
|
|
103
|
-
resizeEvents: this.resizeEvents,
|
|
104
|
-
bufferLines: active.length,
|
|
105
|
-
baseY: active.baseY,
|
|
106
|
-
cursorY: active.cursorY,
|
|
107
|
-
cursorX: active.cursorX,
|
|
108
|
-
lastEvents: [...this.lastEvents],
|
|
109
|
-
tailPreview: this.previewText(snapshot.lines.slice(-tailLines).join('\n'))
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
dispose() {
|
|
114
|
-
if (this.term) {
|
|
115
|
-
this.term.dispose();
|
|
116
|
-
this.term = null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
hookBufferTrim() {
|
|
121
|
-
const lines = this.term && this.term._core && this.term._core.buffer && this.term._core.buffer.lines;
|
|
122
|
-
if (!lines || typeof lines.trimStart !== 'function') return;
|
|
123
|
-
|
|
124
|
-
const originalPush = lines.push.bind(lines);
|
|
125
|
-
lines.push = (value) => {
|
|
126
|
-
if (lines.isFull) {
|
|
127
|
-
const line = lines.get(0);
|
|
128
|
-
this.archiveLines([line ? line.translateToString(true) : '']);
|
|
129
|
-
}
|
|
130
|
-
return originalPush(value);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
if (typeof lines.recycle === 'function') {
|
|
134
|
-
const originalRecycle = lines.recycle.bind(lines);
|
|
135
|
-
lines.recycle = () => {
|
|
136
|
-
if (lines.isFull) {
|
|
137
|
-
const line = lines.get(0);
|
|
138
|
-
this.archiveLines([line ? line.translateToString(true) : '']);
|
|
139
|
-
}
|
|
140
|
-
return originalRecycle();
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const originalTrimStart = lines.trimStart.bind(lines);
|
|
145
|
-
lines.trimStart = (amount) => {
|
|
146
|
-
const removed = [];
|
|
147
|
-
for (let i = 0; i < amount; i++) {
|
|
148
|
-
const line = lines.get(i);
|
|
149
|
-
removed.push(line ? line.translateToString(true) : '');
|
|
150
|
-
}
|
|
151
|
-
this.archiveLines(removed);
|
|
152
|
-
return originalTrimStart(amount);
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
archiveLines(lines) {
|
|
157
|
-
if (!lines || lines.length === 0) return;
|
|
158
|
-
for (const line of lines) {
|
|
159
|
-
this.archivedLines.push(line);
|
|
160
|
-
this.archivedBytes += Buffer.byteLength(line, 'utf8') + 1;
|
|
161
|
-
}
|
|
162
|
-
this.trimArchivedBytes();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
buildSnapshot() {
|
|
166
|
-
const active = this.term.buffer.active;
|
|
167
|
-
let lines = [...this.archivedLines];
|
|
168
|
-
|
|
169
|
-
for (let i = 0; i < active.length; i++) {
|
|
170
|
-
const line = active.getLine(i);
|
|
171
|
-
lines.push(line ? line.translateToString(true) : '');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let truncated = false;
|
|
175
|
-
|
|
176
|
-
let bytes = Buffer.byteLength(lines.join('\n'), 'utf8');
|
|
177
|
-
while (bytes > this.maxBytes && lines.length > 1) {
|
|
178
|
-
const removed = lines.shift();
|
|
179
|
-
bytes -= Buffer.byteLength(removed, 'utf8') + 1;
|
|
180
|
-
truncated = true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (bytes > this.maxBytes && lines.length === 1) {
|
|
184
|
-
const line = lines[0];
|
|
185
|
-
const keepChars = Math.max(1, Math.floor(this.maxBytes / 2));
|
|
186
|
-
lines[0] = line.slice(-keepChars);
|
|
187
|
-
bytes = Buffer.byteLength(lines[0], 'utf8');
|
|
188
|
-
truncated = true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const text = lines.join('\n').replace(/\s+$/g, '');
|
|
192
|
-
this.truncated = truncated;
|
|
193
|
-
return {
|
|
194
|
-
lines,
|
|
195
|
-
text,
|
|
196
|
-
bytes: Buffer.byteLength(text, 'utf8'),
|
|
197
|
-
truncated
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
recordEvent(message) {
|
|
202
|
-
this.lastEvents.push(`${new Date().toISOString()} ${message}`);
|
|
203
|
-
if (this.lastEvents.length > 25) this.lastEvents.shift();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
trimArchivedBytes() {
|
|
207
|
-
while (this.archivedBytes > this.maxBytes && this.archivedLines.length > 0) {
|
|
208
|
-
const removed = this.archivedLines.shift();
|
|
209
|
-
this.archivedBytes -= Buffer.byteLength(removed, 'utf8') + 1;
|
|
210
|
-
this.truncated = true;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
previewText(text, maxChars = 400) {
|
|
215
|
-
if (!text) return '';
|
|
216
|
-
const normalized = String(text)
|
|
217
|
-
.replace(/\r/g, '\\r')
|
|
218
|
-
.replace(/\n/g, '\\n')
|
|
219
|
-
.replace(/\t/g, '\\t')
|
|
220
|
-
.replace(/\x1b/g, '\\x1b');
|
|
221
|
-
return normalized.length > maxChars ? normalized.slice(-maxChars) : normalized;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
module.exports = RenderedHistory;
|