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.
@@ -0,0 +1,74 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+
6
+ const LOG_DIR = path.join(os.homedir(), '.glad', 'logs');
7
+ const LOG_FILE = path.join(LOG_DIR, 'cli.log');
8
+
9
+ // Ensure log directory exists
10
+ function ensureLogDir() {
11
+ if (!fs.existsSync(LOG_DIR)) {
12
+ fs.mkdirSync(LOG_DIR, { recursive: true });
13
+ }
14
+ }
15
+
16
+ // Format timestamp
17
+ function timestamp() {
18
+ return new Date().toISOString().replace('T', ' ').substring(0, 19);
19
+ }
20
+
21
+ // Write to log file
22
+ function writeToFile(level, message) {
23
+ try {
24
+ ensureLogDir();
25
+ const entry = `[${timestamp()}] ${level}: ${message}\n`;
26
+ fs.appendFileSync(LOG_FILE, entry);
27
+ } catch (err) {
28
+ // Silently fail if can't write to log
29
+ }
30
+ }
31
+
32
+ const logger = {
33
+ debug: (message) => {
34
+ // Only log to console and file when DEBUG mode is enabled
35
+ if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
36
+ console.log(chalk.gray(`[DEBUG] ${message}`));
37
+ writeToFile('DEBUG', message);
38
+ }
39
+ },
40
+
41
+ // Always write to file, but only show on console in DEBUG mode
42
+ debugInfo: (message) => {
43
+ writeToFile('INFO', message);
44
+ if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
45
+ console.log(chalk.gray(`[DEBUG] ${message}`));
46
+ }
47
+ },
48
+
49
+ info: (message) => {
50
+ console.log(chalk.blue(`ℹ ${message}`));
51
+ writeToFile('INFO', message);
52
+ },
53
+
54
+ success: (message) => {
55
+ console.log(chalk.green(`✓ ${message}`));
56
+ writeToFile('INFO', message);
57
+ },
58
+
59
+ warn: (message) => {
60
+ console.log(chalk.yellow(`⚠️ ${message}`));
61
+ writeToFile('WARN', message);
62
+ },
63
+
64
+ error: (message) => {
65
+ console.error(chalk.red(`❌ ${message}`));
66
+ writeToFile('ERROR', message);
67
+ },
68
+
69
+ log: (message) => {
70
+ console.log(message);
71
+ }
72
+ };
73
+
74
+ module.exports = logger;
@@ -0,0 +1,67 @@
1
+ // Check if process with PID exists
2
+ function isPidAlive(pid) {
3
+ if (!pid || typeof pid !== 'number') {
4
+ return false;
5
+ }
6
+
7
+ try {
8
+ // Signal 0 checks if process exists without sending actual signal
9
+ process.kill(pid, 0);
10
+ return true;
11
+ } catch (err) {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ // Kill process by PID
17
+ function killProcess(pid, signal = 'SIGTERM') {
18
+ if (!pid || typeof pid !== 'number') {
19
+ throw new Error('Invalid PID');
20
+ }
21
+
22
+ try {
23
+ process.kill(pid, signal);
24
+ return true;
25
+ } catch (err) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ // Kill process gracefully with timeout
31
+ async function killProcessGracefully(pid, timeout = 5000) {
32
+ if (!isPidAlive(pid)) {
33
+ return { success: true, method: 'already_dead' };
34
+ }
35
+
36
+ // Send SIGTERM
37
+ killProcess(pid, 'SIGTERM');
38
+
39
+ // Wait for process to exit
40
+ const startTime = Date.now();
41
+ while (Date.now() - startTime < timeout) {
42
+ if (!isPidAlive(pid)) {
43
+ return { success: true, method: 'SIGTERM' };
44
+ }
45
+ await new Promise(resolve => setTimeout(resolve, 100));
46
+ }
47
+
48
+ // Force kill if still alive
49
+ if (isPidAlive(pid)) {
50
+ killProcess(pid, 'SIGKILL');
51
+ await new Promise(resolve => setTimeout(resolve, 100));
52
+
53
+ if (!isPidAlive(pid)) {
54
+ return { success: true, method: 'SIGKILL' };
55
+ }
56
+
57
+ return { success: false, method: 'failed' };
58
+ }
59
+
60
+ return { success: true, method: 'SIGTERM' };
61
+ }
62
+
63
+ module.exports = {
64
+ isPidAlive,
65
+ killProcess,
66
+ killProcessGracefully
67
+ };
@@ -0,0 +1,53 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Validate directory exists
5
+ function validateDirectory(dirPath) {
6
+ if (!dirPath) {
7
+ return { valid: false, error: 'Directory path is required' };
8
+ }
9
+
10
+ const resolvedPath = path.resolve(dirPath);
11
+
12
+ if (!fs.existsSync(resolvedPath)) {
13
+ return {
14
+ valid: false,
15
+ error: `Directory does not exist: ${resolvedPath}`
16
+ };
17
+ }
18
+
19
+ const stats = fs.statSync(resolvedPath);
20
+ if (!stats.isDirectory()) {
21
+ return {
22
+ valid: false,
23
+ error: `Path is not a directory: ${resolvedPath}`
24
+ };
25
+ }
26
+
27
+ return { valid: true, path: resolvedPath };
28
+ }
29
+
30
+ // Validate URL
31
+ function validateUrl(url) {
32
+ try {
33
+ new URL(url);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ // Validate session ID format
41
+ function validateSessionId(sessionId) {
42
+ if (!sessionId || typeof sessionId !== 'string') {
43
+ return false;
44
+ }
45
+ // UUID format
46
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
47
+ }
48
+
49
+ module.exports = {
50
+ validateDirectory,
51
+ validateUrl,
52
+ validateSessionId
53
+ };
@@ -0,0 +1,273 @@
1
+ class GitGraphRenderer {
2
+ constructor(container) {
3
+ this.container = container;
4
+ this.colors = ['#ff5252', '#448aff', '#69f0ae', '#ffd740', '#e040fb', '#ffab40', '#18ffff'];
5
+ this.rowHeight = 36;
6
+ this.dotRadius = 5;
7
+ this.lineWidth = 2.5;
8
+ }
9
+
10
+ render(commits) {
11
+ this.container.innerHTML = '';
12
+ if (!commits || commits.length === 0) {
13
+ this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">No commits found.</div>';
14
+ return;
15
+ }
16
+
17
+ let tracks = [];
18
+
19
+ for (const commit of commits) {
20
+ const hash = commit.hash;
21
+ const parents = commit.parents;
22
+
23
+ let col = tracks.indexOf(hash);
24
+ let isNewTrack = false;
25
+ if (col === -1) {
26
+ col = tracks.findIndex(t => t === null);
27
+ if (col === -1) col = tracks.length;
28
+ tracks[col] = hash;
29
+ isNewTrack = true;
30
+ }
31
+
32
+ commit.col = col;
33
+ commit.tracksBefore = [...tracks];
34
+ if (isNewTrack) {
35
+ commit.tracksBefore[col] = null;
36
+ }
37
+
38
+ if (parents.length > 0) {
39
+ tracks[col] = parents[0];
40
+ for (let i = 1; i < parents.length; i++) {
41
+ const p = parents[i];
42
+ let pCol = tracks.indexOf(p);
43
+ if (pCol === -1) {
44
+ let eCol = tracks.findIndex(t => t === null);
45
+ if (eCol === -1) eCol = tracks.length;
46
+ tracks[eCol] = p;
47
+ }
48
+ }
49
+ } else {
50
+ tracks[col] = null;
51
+ }
52
+
53
+ for (let i = 0; i < tracks.length; i++) {
54
+ for (let j = i + 1; j < tracks.length; j++) {
55
+ if (tracks[i] && tracks[i] === tracks[j]) {
56
+ tracks[j] = null;
57
+ }
58
+ }
59
+ }
60
+
61
+ while (tracks.length > 0 && tracks[tracks.length - 1] === null) {
62
+ tracks.pop();
63
+ }
64
+
65
+ commit.tracksAfter = [...tracks];
66
+ }
67
+
68
+ const maxTracks = Math.max(...commits.map(c => Math.max(c.tracksBefore.length, c.tracksAfter.length)));
69
+ const canvasWidth = Math.max(50, maxTracks * 16 + 24);
70
+
71
+ commits.forEach((commit, rowIndex) => {
72
+ const rowDiv = document.createElement('div');
73
+ rowDiv.className = 'gitgraph-row';
74
+ rowDiv.style.display = 'flex';
75
+ rowDiv.style.alignItems = 'center';
76
+ rowDiv.style.height = this.rowHeight + 'px';
77
+ rowDiv.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
78
+ rowDiv.style.position = 'relative';
79
+ rowDiv.style.cursor = 'pointer';
80
+ rowDiv.onmouseover = () => rowDiv.style.backgroundColor = 'rgba(255,255,255,0.05)';
81
+ rowDiv.onmouseout = () => rowDiv.style.backgroundColor = 'transparent';
82
+
83
+ const canvas = document.createElement('canvas');
84
+ canvas.width = canvasWidth * 2; // HiDPI
85
+ canvas.height = this.rowHeight * 2;
86
+ canvas.style.flexShrink = '0';
87
+ canvas.style.width = canvasWidth + 'px';
88
+ canvas.style.height = this.rowHeight + 'px';
89
+ rowDiv.appendChild(canvas);
90
+
91
+ this.drawCanvas(canvas, commit);
92
+
93
+ const contentDiv = document.createElement('div');
94
+ contentDiv.className = 'gitgraph-content';
95
+ contentDiv.style.flex = '1';
96
+ contentDiv.style.minWidth = '0';
97
+ contentDiv.style.display = 'flex';
98
+ contentDiv.style.alignItems = 'center';
99
+ contentDiv.style.gap = '8px';
100
+ contentDiv.style.paddingRight = '14px';
101
+ contentDiv.style.whiteSpace = 'nowrap';
102
+ contentDiv.style.overflow = 'hidden';
103
+
104
+ const msgSpan = document.createElement('span');
105
+ msgSpan.style.color = '#fff';
106
+ msgSpan.style.fontSize = '13px';
107
+ msgSpan.style.fontWeight = '500';
108
+ msgSpan.style.overflow = 'hidden';
109
+ msgSpan.style.textOverflow = 'ellipsis';
110
+ msgSpan.textContent = commit.subject;
111
+ contentDiv.appendChild(msgSpan);
112
+
113
+ if (commit.refs) {
114
+ const refsSpan = document.createElement('span');
115
+ refsSpan.style.color = 'var(--primary)';
116
+ refsSpan.style.fontSize = '11px';
117
+ refsSpan.style.border = '1px solid rgba(0, 122, 255, 0.4)';
118
+ refsSpan.style.background = 'rgba(0, 122, 255, 0.1)';
119
+ refsSpan.style.borderRadius = '4px';
120
+ refsSpan.style.padding = '1px 4px';
121
+ refsSpan.style.flexShrink = '0';
122
+ refsSpan.textContent = commit.refs.replace(/[()]/g, '').trim();
123
+ contentDiv.appendChild(refsSpan);
124
+ }
125
+
126
+ const authorSpan = document.createElement('span');
127
+ authorSpan.style.color = 'var(--text-dim)';
128
+ authorSpan.style.fontSize = '12px';
129
+ authorSpan.style.marginLeft = 'auto';
130
+ authorSpan.style.flexShrink = '0';
131
+ authorSpan.textContent = `${commit.author} • ${commit.time}`;
132
+ contentDiv.appendChild(authorSpan);
133
+
134
+ const hashSpan = document.createElement('span');
135
+ hashSpan.style.color = 'var(--text-dim)';
136
+ hashSpan.style.fontSize = '12px';
137
+ hashSpan.style.fontFamily = 'monospace';
138
+ hashSpan.style.flexShrink = '0';
139
+ hashSpan.style.width = '60px';
140
+ hashSpan.style.textAlign = 'right';
141
+ hashSpan.textContent = commit.hash;
142
+ contentDiv.appendChild(hashSpan);
143
+
144
+
145
+ rowDiv.onclick = (e) => {
146
+ if (e.target.closest('.gitgraph-details-view')) return;
147
+
148
+ const existingDetails = rowDiv.nextElementSibling;
149
+ if (existingDetails && existingDetails.classList.contains('gitgraph-details-view')) {
150
+ existingDetails.remove();
151
+ rowDiv.style.backgroundColor = 'transparent';
152
+ return;
153
+ }
154
+
155
+ // Close others
156
+ document.querySelectorAll('.gitgraph-details-view').forEach(el => {
157
+ if (el.previousElementSibling) {
158
+ el.previousElementSibling.style.backgroundColor = 'transparent';
159
+ }
160
+ el.remove();
161
+ });
162
+
163
+ rowDiv.style.backgroundColor = 'rgba(255,255,255,0.05)';
164
+
165
+ const detailsDiv = document.createElement('div');
166
+ detailsDiv.className = 'gitgraph-details-view';
167
+ detailsDiv.style.backgroundColor = '#121212';
168
+ detailsDiv.style.borderLeft = '2px solid ' + this.getColor(commit.col);
169
+ detailsDiv.style.padding = '16px';
170
+ detailsDiv.style.margin = '4px 0 4px ' + (canvasWidth) + 'px';
171
+ detailsDiv.style.borderRadius = '0 6px 6px 0';
172
+ detailsDiv.style.fontSize = '13px';
173
+ detailsDiv.style.color = 'var(--text)';
174
+ detailsDiv.style.boxShadow = 'inset 0 0 10px rgba(0,0,0,0.5)';
175
+
176
+ let detailsHTML = `
177
+ <div style="display:flex; justify-content:space-between; margin-bottom:12px;">
178
+ <div>
179
+ <div style="font-weight:600; font-size:14px; margin-bottom:4px;">${commit.subject.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</div>
180
+ <div style="color:var(--text-dim);">${commit.author} commited ${commit.time}</div>
181
+ </div>
182
+ <div style="text-align:right;">
183
+ <div style="font-family:monospace; color:var(--text-dim);">Commit: ${commit.hash}</div>
184
+ ${commit.parents.length > 0 ? `<div style="font-family:monospace; color:var(--text-dim);">Parents: ${commit.parents.join(', ')}</div>` : ''}
185
+ </div>
186
+ </div>
187
+ `;
188
+
189
+ // Add diff placeholder
190
+ detailsHTML += `
191
+ <div style="border-top:1px solid #333; padding-top:12px; margin-top:12px;">
192
+ <button onclick="window.loadCommitDiff('${commit.hash}', this)" style="background:var(--primary); border:none; color:#fff; padding:6px 12px; border-radius:4px; font-size:12px; cursor:pointer;">Load Diff</button>
193
+ <div class="diff-container" style="margin-top:12px; font-family:monospace; font-size:12px; white-space:pre-wrap; overflow-x:auto;"></div>
194
+ </div>
195
+ `;
196
+
197
+ detailsDiv.innerHTML = detailsHTML;
198
+ rowDiv.parentNode.insertBefore(detailsDiv, rowDiv.nextSibling);
199
+ };
200
+
201
+ rowDiv.appendChild(contentDiv);
202
+
203
+ this.container.appendChild(rowDiv);
204
+ });
205
+ }
206
+
207
+ drawCanvas(canvas, commit) {
208
+ const ctx = canvas.getContext('2d');
209
+ ctx.scale(2, 2); // HiDPI
210
+
211
+ const h = this.rowHeight;
212
+ const w = canvas.width / 2;
213
+ const midY = h / 2;
214
+ const colSpacing = 16;
215
+ const startX = 20;
216
+
217
+ // Pass-through lines
218
+ commit.tracksBefore.forEach((target, i) => {
219
+ if (!target) return;
220
+ const x1 = startX + i * colSpacing;
221
+
222
+ if (target === commit.hash) {
223
+ const x2 = startX + commit.col * colSpacing;
224
+ this.drawLine(ctx, x1, 0, x2, midY, this.getColor(i));
225
+ } else {
226
+ const nextIdx = commit.tracksAfter.indexOf(target);
227
+ if (nextIdx !== -1) {
228
+ const x2 = startX + nextIdx * colSpacing;
229
+ this.drawLine(ctx, x1, 0, x2, h, this.getColor(i));
230
+ }
231
+ }
232
+ });
233
+
234
+ // Lines for parents
235
+ commit.parents.forEach((parent, parentIdx) => {
236
+ const nextIdx = commit.tracksAfter.indexOf(parent);
237
+ if (nextIdx !== -1) {
238
+ const x1 = startX + commit.col * colSpacing;
239
+ const x2 = startX + nextIdx * colSpacing;
240
+ const color = parentIdx === 0 ? this.getColor(commit.col) : this.getColor(nextIdx);
241
+ this.drawLine(ctx, x1, midY, x2, h, color);
242
+ }
243
+ });
244
+
245
+ // Commit dot
246
+ const cx = startX + commit.col * colSpacing;
247
+ ctx.beginPath();
248
+ ctx.arc(cx, midY, this.dotRadius, 0, 2 * Math.PI);
249
+ ctx.fillStyle = this.getColor(commit.col);
250
+ ctx.fill();
251
+ ctx.lineWidth = 2;
252
+ ctx.strokeStyle = '#121212'; // dark background match
253
+ ctx.stroke();
254
+ }
255
+
256
+ drawLine(ctx, x1, y1, x2, y2, color) {
257
+ ctx.beginPath();
258
+ ctx.moveTo(x1, y1);
259
+ if (x1 === x2) {
260
+ ctx.lineTo(x2, y2);
261
+ } else {
262
+ ctx.bezierCurveTo(x1, (y1 + y2) / 2, x2, (y1 + y2) / 2, x2, y2);
263
+ }
264
+ ctx.strokeStyle = color;
265
+ ctx.lineWidth = this.lineWidth;
266
+ ctx.stroke();
267
+ }
268
+
269
+ getColor(index) {
270
+ return this.colors[index % this.colors.length];
271
+ }
272
+ }
273
+ window.GitGraphRenderer = GitGraphRenderer;