glad-web 1.0.7
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/LICENSE +21 -0
- package/README.md +182 -0
- package/README.zh-CN.md +182 -0
- package/assets/logo.svg +40 -0
- package/bin/cli.js +65 -0
- package/lib/ai-tools/demo/enhanced-demo.js +625 -0
- package/lib/ai-tools/demo/index.js +24 -0
- package/lib/ai-tools/demo/responses.js +88 -0
- package/lib/ai-tools/detector.js +76 -0
- package/lib/ai-tools/registry.js +300 -0
- package/lib/commands/config.js +78 -0
- package/lib/commands/tools.js +128 -0
- package/lib/commands/web.js +454 -0
- package/lib/config/constants.js +17 -0
- package/lib/config/manager.js +71 -0
- package/lib/git/service.js +79 -0
- package/lib/schedule/job-runner.js +162 -0
- package/lib/schedule/job-store.js +150 -0
- package/lib/schedule/key-sequences.js +49 -0
- package/lib/schedule/scheduler-service.js +39 -0
- package/lib/session/buffer.js +102 -0
- package/lib/session/pty-manager.js +241 -0
- package/lib/session/rendered-history.js +225 -0
- package/lib/session/session-manager.js +382 -0
- package/lib/session/text-history.js +274 -0
- package/lib/utils/logger.js +74 -0
- package/lib/utils/pid.js +67 -0
- package/lib/utils/validation.js +53 -0
- package/lib/web/gitgraph.js +273 -0
- package/lib/web/index.html +1470 -0
- package/lib/workspace/service.js +76 -0
- package/package.json +60 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { WebSocketServer } = require('ws');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { getAllTools } = require('../ai-tools/registry');
|
|
10
|
+
const { GitService } = require('../git/service');
|
|
11
|
+
const WorkspaceService = require('../workspace/service');
|
|
12
|
+
const SessionManager = require('../session/session-manager');
|
|
13
|
+
|
|
14
|
+
function sendCompressedJson(req, res, payload) {
|
|
15
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
16
|
+
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
17
|
+
|
|
18
|
+
if (/\bgzip\b/.test(acceptEncoding)) {
|
|
19
|
+
zlib.gzip(body, { level: 6 }, (error, compressed) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
res.type('application/json').send(body);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
25
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
26
|
+
res.setHeader('Vary', 'Accept-Encoding');
|
|
27
|
+
res.setHeader('Content-Length', compressed.length);
|
|
28
|
+
res.send(compressed);
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
res.type('application/json').send(body);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { detectInstalledTools } = require('../ai-tools/detector');
|
|
37
|
+
const logger = require('../utils/logger');
|
|
38
|
+
const { JobStore } = require('../schedule/job-store');
|
|
39
|
+
const JobRunner = require('../schedule/job-runner');
|
|
40
|
+
const SchedulerService = require('../schedule/scheduler-service');
|
|
41
|
+
|
|
42
|
+
async function webCommand(options) {
|
|
43
|
+
const port = parseInt(options.port) || 3000;
|
|
44
|
+
const debugHistoryEnabled = process.env.DEBUG_SESSION_HISTORY === '1';
|
|
45
|
+
const defaultRenderedTools = getAllTools().map(tool => tool.key).join(',');
|
|
46
|
+
const renderHistoryTools = new Set(
|
|
47
|
+
String(process.env.HISTORY_RENDER_TOOLS || defaultRenderedTools)
|
|
48
|
+
.split(',')
|
|
49
|
+
.map(value => value.trim().toLowerCase())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
);
|
|
52
|
+
const app = express();
|
|
53
|
+
app.use((req, res, next) => {
|
|
54
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
55
|
+
res.setHeader('Pragma', 'no-cache');
|
|
56
|
+
res.setHeader('Expires', '0');
|
|
57
|
+
next();
|
|
58
|
+
});
|
|
59
|
+
app.use(express.json());
|
|
60
|
+
const server = http.createServer(app);
|
|
61
|
+
const wss = new WebSocketServer({
|
|
62
|
+
server,
|
|
63
|
+
perMessageDeflate: {
|
|
64
|
+
threshold: 1024,
|
|
65
|
+
zlibDeflateOptions: { level: 3 },
|
|
66
|
+
zlibInflateOptions: {},
|
|
67
|
+
clientNoContextTakeover: true,
|
|
68
|
+
serverNoContextTakeover: true
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Use directory from options if provided, otherwise default to current working directory
|
|
73
|
+
const baseDir = options.directory ? path.resolve(process.cwd(), options.directory) : process.cwd();
|
|
74
|
+
|
|
75
|
+
const jobStore = new JobStore();
|
|
76
|
+
const gitService = new GitService();
|
|
77
|
+
const workspaceService = new WorkspaceService({ gitService });
|
|
78
|
+
const sessionManager = new SessionManager({
|
|
79
|
+
baseDir,
|
|
80
|
+
renderHistoryTools,
|
|
81
|
+
debugHistoryEnabled,
|
|
82
|
+
logger,
|
|
83
|
+
hasConnectedSessionClient
|
|
84
|
+
});
|
|
85
|
+
sessionManager.on('output', ({ sessionId, data }) => {
|
|
86
|
+
broadcastToSession(sessionId, { type: 'output', data });
|
|
87
|
+
});
|
|
88
|
+
sessionManager.on('exit', ({ sessionId }) => {
|
|
89
|
+
broadcastToSession(sessionId, { type: 'exit' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const jobRunner = new JobRunner({
|
|
93
|
+
createSession: input => sessionManager.create(input),
|
|
94
|
+
getJob: id => jobStore.get(id),
|
|
95
|
+
updateJob: (id, patch) => jobStore.patchRuntime(id, patch),
|
|
96
|
+
logger
|
|
97
|
+
});
|
|
98
|
+
const schedulerService = new SchedulerService({ jobStore, jobRunner, logger });
|
|
99
|
+
schedulerService.start();
|
|
100
|
+
|
|
101
|
+
// API: Get all supported and installed tools
|
|
102
|
+
app.get('/api/tools', async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const tools = await detectInstalledTools();
|
|
105
|
+
res.json(tools);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
res.status(500).json({ error: 'Failed to detect tools' });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// API: Get web UI runtime configuration
|
|
112
|
+
app.get('/api/config', (req, res) => {
|
|
113
|
+
res.json({ defaultWorkingDirectory: baseDir });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// API: Scheduled tasks
|
|
117
|
+
app.get('/api/schedules', (req, res) => {
|
|
118
|
+
res.json(jobStore.list());
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.post('/api/schedules', (req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
const job = jobStore.create(req.body || {});
|
|
124
|
+
res.json(job);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
res.status(400).json({ error: e.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.get('/api/schedules/:id', (req, res) => {
|
|
131
|
+
const job = jobStore.get(req.params.id);
|
|
132
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
133
|
+
res.json(job);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.patch('/api/schedules/:id', (req, res) => {
|
|
137
|
+
const job = jobStore.update(req.params.id, req.body || {});
|
|
138
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
139
|
+
res.json(job);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app.patch('/api/schedules/:id/enabled', (req, res) => {
|
|
143
|
+
const job = jobStore.patchRuntime(req.params.id, { enabled: Boolean(req.body && req.body.enabled) });
|
|
144
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
145
|
+
res.json(job);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.delete('/api/schedules/:id', (req, res) => {
|
|
149
|
+
res.json({ success: jobStore.delete(req.params.id) });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.post('/api/schedules/:id/duplicate', (req, res) => {
|
|
153
|
+
const job = jobStore.duplicate(req.params.id);
|
|
154
|
+
if (!job) return res.status(404).json({ error: 'Scheduled task not found' });
|
|
155
|
+
res.json(job);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
app.post('/api/schedules/:id/run', async (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await jobRunner.run(req.params.id, { manual: false });
|
|
161
|
+
res.json(result);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
res.status(400).json({ error: e.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.post('/api/schedules/:id/simulate', async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const result = await jobRunner.run(req.params.id, { manual: true, background: true });
|
|
170
|
+
res.json(result);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
res.status(400).json({ error: e.message });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// API: List all active sessions
|
|
177
|
+
app.get('/api/sessions', (req, res) => {
|
|
178
|
+
logger.debug('API: GET /api/sessions');
|
|
179
|
+
res.json(sessionManager.list());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// API: Create a new PTY session
|
|
183
|
+
app.post('/api/sessions', async (req, res) => {
|
|
184
|
+
logger.debug(`API: POST /api/sessions - ${JSON.stringify(req.body)}`);
|
|
185
|
+
try {
|
|
186
|
+
const { toolKey, workingDirectory } = req.body;
|
|
187
|
+
const session = sessionManager.create({ toolKey, workingDirectory });
|
|
188
|
+
res.json({ id: session.id });
|
|
189
|
+
} catch (e) {
|
|
190
|
+
logger.error(`API: POST /api/sessions failed: ${e.message}`);
|
|
191
|
+
res.status(e.statusCode || 500).json({ error: e.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// API: Plain text terminal history for mobile-friendly reading
|
|
196
|
+
app.get('/api/sessions/:id/history', (req, res) => {
|
|
197
|
+
const history = sessionManager.getHistory(req.params.id);
|
|
198
|
+
if (!history) return res.status(404).json({ error: 'Session not found' });
|
|
199
|
+
sessionManager.logHistoryRequest(req.params.id, req);
|
|
200
|
+
sendCompressedJson(req, res, history);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// API: Rename session
|
|
204
|
+
app.patch('/api/sessions/:id', (req, res) => {
|
|
205
|
+
const session = sessionManager.rename(req.params.id, req.body.name);
|
|
206
|
+
if (session) {
|
|
207
|
+
res.json({ success: true, name: session.name });
|
|
208
|
+
} else {
|
|
209
|
+
res.status(404).json({ error: 'Session not found' });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// API: Mark a session completion indicator as read
|
|
214
|
+
app.post('/api/sessions/:id/completion/read', (req, res) => {
|
|
215
|
+
const session = sessionManager.markCompletionRead(req.params.id);
|
|
216
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
217
|
+
res.json({ success: true });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// API: Delete/Kill session
|
|
221
|
+
app.delete('/api/sessions/:id', (req, res) => {
|
|
222
|
+
sessionManager.kill(req.params.id);
|
|
223
|
+
res.json({ success: true });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
app.get('/api/sessions/:id/debug', (req, res) => {
|
|
227
|
+
const diagnostics = sessionManager.getDiagnostics(req.params.id);
|
|
228
|
+
if (!diagnostics) return res.status(404).json({ error: 'Session not found' });
|
|
229
|
+
res.json({ success: true, diagnostics });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
app.post('/api/debug/client-log', (req, res) => {
|
|
233
|
+
const { sessionId, event, payload } = req.body || {};
|
|
234
|
+
sessionManager.logClientDebug(sessionId, event, payload);
|
|
235
|
+
res.json({ success: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// API: Git Show
|
|
239
|
+
app.get('/api/sessions/:id/git-show/:hash', async (req, res) => {
|
|
240
|
+
const session = sessionManager.get(req.params.id);
|
|
241
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
242
|
+
const hash = req.params.hash;
|
|
243
|
+
const result = await gitService.show(session.ptyManager.workingDir, hash);
|
|
244
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// API: Git Log
|
|
248
|
+
app.get('/api/sessions/:id/git-log', async (req, res) => {
|
|
249
|
+
const session = sessionManager.get(req.params.id);
|
|
250
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
251
|
+
const result = await gitService.log(session.ptyManager.workingDir, req.query.maxCount);
|
|
252
|
+
if (!result.success) {
|
|
253
|
+
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
254
|
+
}
|
|
255
|
+
res.json({ success: true, commits: result.commits });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// API: Git Status
|
|
259
|
+
app.get('/api/sessions/:id/git-status', async (req, res) => {
|
|
260
|
+
const session = sessionManager.get(req.params.id);
|
|
261
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
262
|
+
const result = await gitService.status(session.ptyManager.workingDir);
|
|
263
|
+
if (!result.success) {
|
|
264
|
+
return res.status(500).json({ error: result.error, stderr: result.stderr });
|
|
265
|
+
}
|
|
266
|
+
res.json({ success: true, files: result.files });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// API: Git Diff Numstat (unstaged and staged)
|
|
270
|
+
app.get('/api/sessions/:id/git-diff-numstat', async (req, res) => {
|
|
271
|
+
const session = sessionManager.get(req.params.id);
|
|
272
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
273
|
+
const isStaged = req.query.staged === 'true';
|
|
274
|
+
const result = await gitService.diffNumstat(session.ptyManager.workingDir, isStaged);
|
|
275
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// API: Git Diff File
|
|
279
|
+
app.get('/api/sessions/:id/git-diff-file', async (req, res) => {
|
|
280
|
+
const session = sessionManager.get(req.params.id);
|
|
281
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
282
|
+
const isStaged = req.query.staged === 'true';
|
|
283
|
+
const filePath = req.query.path;
|
|
284
|
+
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
285
|
+
const result = await gitService.diffFile(session.ptyManager.workingDir, filePath, isStaged);
|
|
286
|
+
res.json({ success: result.success, stdout: result.stdout, stderr: result.stderr });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// API: Get File Content
|
|
290
|
+
app.get('/api/sessions/:id/file', async (req, res) => {
|
|
291
|
+
const session = sessionManager.get(req.params.id);
|
|
292
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
293
|
+
const filePath = req.query.path || '';
|
|
294
|
+
if (!filePath) return res.status(400).json({ error: 'Missing file path' });
|
|
295
|
+
const cwd = session.ptyManager.workingDir || '';
|
|
296
|
+
try {
|
|
297
|
+
const content = workspaceService.readFile(cwd, filePath);
|
|
298
|
+
res.json({ success: true, content });
|
|
299
|
+
} catch (e) {
|
|
300
|
+
res.status(e.statusCode || 200).json({ success: false, error: e.message });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// API: Get Directory Contents
|
|
305
|
+
app.get('/api/sessions/:id/fs/dir', async (req, res) => {
|
|
306
|
+
const session = sessionManager.get(req.params.id);
|
|
307
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
308
|
+
const dirPath = req.query.path || '';
|
|
309
|
+
const cwd = session.ptyManager.workingDir || '';
|
|
310
|
+
try {
|
|
311
|
+
const files = await workspaceService.listDirectory(cwd, dirPath);
|
|
312
|
+
res.json({ success: true, files });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
res.status(e.statusCode || 200).json({ success: false, error: e.message });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
function broadcastToSession(sessionId, message) {
|
|
320
|
+
const msgStr = JSON.stringify(message);
|
|
321
|
+
wss.clients.forEach(client => {
|
|
322
|
+
if (client.readyState === 1 && client.sessionId === sessionId) {
|
|
323
|
+
client.send(msgStr);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function hasConnectedSessionClient(sessionId) {
|
|
329
|
+
for (const client of wss.clients) {
|
|
330
|
+
if (client.readyState === 1 && client.sessionId === sessionId) return true;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// WebSocket: Terminal I/O
|
|
336
|
+
wss.on('connection', (ws, req) => {
|
|
337
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
338
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
339
|
+
|
|
340
|
+
if (!sessionId || !sessionManager.has(sessionId)) {
|
|
341
|
+
ws.close(4001, 'Invalid Session ID');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
ws.sessionId = sessionId;
|
|
346
|
+
const session = sessionManager.get(sessionId);
|
|
347
|
+
if (!session.resizeOwner) {
|
|
348
|
+
session.resizeOwner = ws;
|
|
349
|
+
}
|
|
350
|
+
sessionManager.logWsConnected(sessionId, req);
|
|
351
|
+
|
|
352
|
+
// Send catchup buffer
|
|
353
|
+
const history = session.buffer.getAfter(0);
|
|
354
|
+
if (history.length > 0) {
|
|
355
|
+
sessionManager.logWsCatchup(sessionId, history);
|
|
356
|
+
ws.send(JSON.stringify({ type: 'output', data: history.map(m => m.data).join('') }));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
ws.on('message', (message) => {
|
|
360
|
+
try {
|
|
361
|
+
const payload = JSON.parse(message);
|
|
362
|
+
if (payload.type === 'input') {
|
|
363
|
+
session.write(payload.data);
|
|
364
|
+
}
|
|
365
|
+
if (payload.type === 'resize' && session.resizeOwner === ws) {
|
|
366
|
+
sessionManager.logWsResize(sessionId, payload.cols, payload.rows);
|
|
367
|
+
sessionManager.resize(sessionId, payload.cols, payload.rows);
|
|
368
|
+
}
|
|
369
|
+
} catch (e) {
|
|
370
|
+
logger.error('WS Message Error: ' + e.message);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
ws.on('close', () => {
|
|
375
|
+
sessionManager.logWsClosed(sessionId);
|
|
376
|
+
if (session.resizeOwner !== ws) return;
|
|
377
|
+
session.resizeOwner = null;
|
|
378
|
+
for (const client of wss.clients) {
|
|
379
|
+
if (client.readyState === 1 && client.sessionId === sessionId) {
|
|
380
|
+
session.resizeOwner = client;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Frontend routes
|
|
388
|
+
app.get('/', (req, res) => {
|
|
389
|
+
try {
|
|
390
|
+
const htmlPath = path.join(__dirname, '../web/index.html');
|
|
391
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
392
|
+
res.send(html);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
res.status(500).send('UI not found');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
app.get('/gitgraph.js', (req, res) => {
|
|
399
|
+
try {
|
|
400
|
+
res.sendFile(path.join(__dirname, '../web/gitgraph.js'));
|
|
401
|
+
} catch (e) {
|
|
402
|
+
res.status(404).send('Not found');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
app.get('/logo.svg', (req, res) => {
|
|
407
|
+
try {
|
|
408
|
+
res.sendFile(path.resolve(__dirname, '../../assets/logo.svg'));
|
|
409
|
+
} catch (e) {
|
|
410
|
+
res.status(404).send('Not found');
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
app.get('/manifest.json', (req, res) => res.json({
|
|
415
|
+
name: "Glad Web",
|
|
416
|
+
short_name: "Glad",
|
|
417
|
+
start_url: "/",
|
|
418
|
+
display: "standalone",
|
|
419
|
+
background_color: "#000000",
|
|
420
|
+
theme_color: "#007aff",
|
|
421
|
+
icons: [
|
|
422
|
+
{
|
|
423
|
+
src: "/logo.svg",
|
|
424
|
+
sizes: "any",
|
|
425
|
+
type: "image/svg+xml"
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
}));
|
|
429
|
+
|
|
430
|
+
server.listen(port, '0.0.0.0', () => {
|
|
431
|
+
const interfaces = os.networkInterfaces();
|
|
432
|
+
let networkInfo = '';
|
|
433
|
+
for (const name of Object.keys(interfaces)) {
|
|
434
|
+
for (const iface of interfaces[name]) {
|
|
435
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
436
|
+
networkInfo += `\n ➜ Network: http://${iface.address}:${port}`;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
console.log(chalk.green(`\n🚀 Glad Web Server is running!`));
|
|
441
|
+
console.log(chalk.cyan(` ➜ Local: http://localhost:${port}${networkInfo}\n`));
|
|
442
|
+
console.log(chalk.gray(` ➜ Project: ${baseDir}\n`));
|
|
443
|
+
console.log(chalk.gray(` ➜ History Render Tools: ${Array.from(renderHistoryTools).join(', ') || '(none)'}\n`));
|
|
444
|
+
console.log(chalk.gray(`Tips: Access from your phone via the Network URL above.\n`));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
process.on('SIGINT', () => {
|
|
448
|
+
schedulerService.stop();
|
|
449
|
+
sessionManager.killAll();
|
|
450
|
+
process.exit(0);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = webCommand;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application constants
|
|
3
|
+
*
|
|
4
|
+
* All magic numbers and configuration values should be defined here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
// Network & WebSocket
|
|
9
|
+
HEARTBEAT_TIMEOUT: 13000, // 13s - detect network loss (server pings every ~5s)
|
|
10
|
+
CLI_IDLE_THRESHOLD: 15000, // 15s - no PTY output = CLI is idle
|
|
11
|
+
|
|
12
|
+
// Buffer
|
|
13
|
+
DEFAULT_BUFFER_SIZE: 100000, // 100KB - circular buffer max size
|
|
14
|
+
|
|
15
|
+
// Reconnection
|
|
16
|
+
MAX_RECONNECT_ATTEMPTS: 10, // Max WebSocket reconnection attempts
|
|
17
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const Conf = require('conf');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
// Config schema - only user preferences, no environment config
|
|
6
|
+
const schema = {
|
|
7
|
+
defaultAI: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
default: ''
|
|
10
|
+
},
|
|
11
|
+
version: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
default: '1.0.0'
|
|
14
|
+
},
|
|
15
|
+
lastUpdated: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
default: ''
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Create config instance
|
|
22
|
+
const config = new Conf({
|
|
23
|
+
projectName: 'glad',
|
|
24
|
+
cwd: path.join(os.homedir(), '.glad'),
|
|
25
|
+
configName: 'config',
|
|
26
|
+
schema
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Get config value
|
|
30
|
+
function getConfig(key) {
|
|
31
|
+
if (key) {
|
|
32
|
+
return config.get(key);
|
|
33
|
+
}
|
|
34
|
+
return config.store;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Set config value
|
|
38
|
+
function setConfig(key, value) {
|
|
39
|
+
config.set(key, value);
|
|
40
|
+
config.set('lastUpdated', new Date().toISOString());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get default AI tool
|
|
44
|
+
function getDefaultAI() {
|
|
45
|
+
const value = config.get('defaultAI');
|
|
46
|
+
return value || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Set default AI tool
|
|
50
|
+
function setDefaultAI(tool) {
|
|
51
|
+
setConfig('defaultAI', tool);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get config file path
|
|
55
|
+
function getConfigPath() {
|
|
56
|
+
return config.path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Reset config to defaults
|
|
60
|
+
function resetConfig() {
|
|
61
|
+
config.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
getConfig,
|
|
66
|
+
setConfig,
|
|
67
|
+
getDefaultAI,
|
|
68
|
+
setDefaultAI,
|
|
69
|
+
getConfigPath,
|
|
70
|
+
resetConfig
|
|
71
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
|
|
3
|
+
function execFilePromise(file, args, cwd) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
execFile(file, args, { cwd, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
6
|
+
resolve({ success: !error, error: error?.message, stdout, stderr });
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseGitStatusZ(stdout) {
|
|
12
|
+
if (!stdout) return [];
|
|
13
|
+
|
|
14
|
+
const entries = [];
|
|
15
|
+
const records = stdout.split('\0').filter(Boolean);
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < records.length; i++) {
|
|
18
|
+
const record = records[i];
|
|
19
|
+
if (record.length < 3) continue;
|
|
20
|
+
|
|
21
|
+
const status = record.substring(0, 2);
|
|
22
|
+
const path = record.substring(3);
|
|
23
|
+
const entry = { path, status };
|
|
24
|
+
|
|
25
|
+
if ((status.includes('R') || status.includes('C')) && i + 1 < records.length) {
|
|
26
|
+
entry.originalPath = records[++i];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
entries.push(entry);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return entries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class GitService {
|
|
36
|
+
async show(cwd, hash) {
|
|
37
|
+
return execFilePromise('git', ['show', '--format=fuller', '--stat', '-p', hash], cwd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async log(cwd, maxCount = 100) {
|
|
41
|
+
const count = Number.parseInt(maxCount, 10) || 100;
|
|
42
|
+
const result = await execFilePromise(
|
|
43
|
+
'git',
|
|
44
|
+
['log', '--all', '--date-order', `--max-count=${count}`, '--pretty=format:%h|%p|%d|%s|%an|%ar'],
|
|
45
|
+
cwd
|
|
46
|
+
);
|
|
47
|
+
if (!result.success) return result;
|
|
48
|
+
|
|
49
|
+
const commits = result.stdout.split('\n').filter(Boolean).map(line => {
|
|
50
|
+
const [hash, parents, refs, subject, author, time] = line.split('|');
|
|
51
|
+
return { hash, parents: parents ? parents.split(' ') : [], refs: refs ? refs.trim() : '', subject, author, time };
|
|
52
|
+
});
|
|
53
|
+
return { ...result, commits };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async status(cwd) {
|
|
57
|
+
const result = await execFilePromise('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], cwd);
|
|
58
|
+
if (!result.success) return result;
|
|
59
|
+
return { ...result, files: parseGitStatusZ(result.stdout) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
diffNumstat(cwd, isStaged = false) {
|
|
63
|
+
const args = isStaged ? ['diff', '--cached', '--numstat'] : ['diff', '--numstat'];
|
|
64
|
+
return execFilePromise('git', args, cwd);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
diffFile(cwd, filePath, isStaged = false) {
|
|
68
|
+
const args = isStaged
|
|
69
|
+
? ['diff', '--cached', '--no-ext-diff', '--', filePath]
|
|
70
|
+
: ['diff', '--no-ext-diff', '--', filePath];
|
|
71
|
+
return execFilePromise('git', args, cwd);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
GitService,
|
|
77
|
+
execFilePromise,
|
|
78
|
+
parseGitStatusZ
|
|
79
|
+
};
|