termbeam 1.15.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +2 -1
- package/public/assets/{_basePickBy-BSIbg2Hw.js → _basePickBy-Crmlna7W.js} +1 -1
- package/public/assets/{_baseUniq-CYmx81nY.js → _baseUniq-h6HY8nD4.js} +1 -1
- package/public/assets/{arc-CDJcNcKc.js → arc-BI4RNUD8.js} +1 -1
- package/public/assets/{architectureDiagram-2XIMDMQ5-C1qauSxh.js → architectureDiagram-2XIMDMQ5-C2PAl3D6.js} +1 -1
- package/public/assets/{blockDiagram-WCTKOSBZ-nTHCaU6g.js → blockDiagram-WCTKOSBZ-CADYyoNx.js} +1 -1
- package/public/assets/{c4Diagram-IC4MRINW-CdGuCZNN.js → c4Diagram-IC4MRINW-CQtNNlqT.js} +1 -1
- package/public/assets/channel-DlFJ0YtH.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-IxfdQ8zN.js → chunk-4BX2VUAB-BZhBHL2q.js} +1 -1
- package/public/assets/{chunk-55IACEB6-mjdLMPLu.js → chunk-55IACEB6-DaOODotQ.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-B-QgE8A5.js → chunk-FMBD7UC4-D7ZUE2Qt.js} +1 -1
- package/public/assets/{chunk-JSJVCQXG-BlBtV3cx.js → chunk-JSJVCQXG-Cr7LmD49.js} +1 -1
- package/public/assets/{chunk-KX2RTZJC-ByLbXYtr.js → chunk-KX2RTZJC-mSzu7V0i.js} +1 -1
- package/public/assets/{chunk-NQ4KR5QH-DdgKg6ac.js → chunk-NQ4KR5QH-UNIo7K3P.js} +1 -1
- package/public/assets/{chunk-QZHKN3VN-DK0sNhO7.js → chunk-QZHKN3VN-D8pHtVTR.js} +1 -1
- package/public/assets/{chunk-WL4C6EOR-CMhwM8MW.js → chunk-WL4C6EOR-CKtSBmtm.js} +1 -1
- package/public/assets/classDiagram-VBA2DB6C-Uh272C_T.js +1 -0
- package/public/assets/classDiagram-v2-RAHNMMFH-Uh272C_T.js +1 -0
- package/public/assets/clone-BiOpyrvc.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-C4J5lbLg.js → cose-bilkent-S5V4N54A-C73dVsDU.js} +1 -1
- package/public/assets/{dagre-KLK3FWXG-CmPYo_iW.js → dagre-KLK3FWXG-CGtdO-e6.js} +1 -1
- package/public/assets/{diagram-E7M64L7V-BSDHjD_1.js → diagram-E7M64L7V-B3RnL1-2.js} +1 -1
- package/public/assets/{diagram-IFDJBPK2-DZFEThmE.js → diagram-IFDJBPK2-BhT13Y--.js} +1 -1
- package/public/assets/{diagram-P4PSJMXO-D2vA458R.js → diagram-P4PSJMXO-w4ta5qzj.js} +1 -1
- package/public/assets/{erDiagram-INFDFZHY-CqngKW80.js → erDiagram-INFDFZHY-p_XdulXc.js} +1 -1
- package/public/assets/{flowDiagram-PKNHOUZH-2ndb8I08.js → flowDiagram-PKNHOUZH-cKD9roCC.js} +1 -1
- package/public/assets/{ganttDiagram-A5KZAMGK-DGH9iwxm.js → ganttDiagram-A5KZAMGK-kRLcbnHy.js} +1 -1
- package/public/assets/{gitGraphDiagram-K3NZZRJ6-DBszyq19.js → gitGraphDiagram-K3NZZRJ6-CfqReYYJ.js} +1 -1
- package/public/assets/{graph-B-VDztTg.js → graph-2Z05uqaC.js} +1 -1
- package/public/assets/index-Bpz9aDGB.css +32 -0
- package/public/assets/index-Cvxh0Fjh.js +394 -0
- package/public/assets/{infoDiagram-LFFYTUFH-BQYostn9.js → infoDiagram-LFFYTUFH-D2bxFvYS.js} +1 -1
- package/public/assets/{ishikawaDiagram-PHBUUO56-BF9SDQjL.js → ishikawaDiagram-PHBUUO56-olWTIvNJ.js} +1 -1
- package/public/assets/{journeyDiagram-4ABVD52K-BVygcg_3.js → journeyDiagram-4ABVD52K-T_3LhARU.js} +1 -1
- package/public/assets/{kanban-definition-K7BYSVSG-C360CZ_M.js → kanban-definition-K7BYSVSG-BCmUNdAK.js} +1 -1
- package/public/assets/{layout-D1dS_Xae.js → layout-BuQ9md8V.js} +1 -1
- package/public/assets/{linear-DSiHoSbJ.js → linear-BGGATdCH.js} +1 -1
- package/public/assets/{mindmap-definition-YRQLILUH-DW7C3qtv.js → mindmap-definition-YRQLILUH-Bz_sgl78.js} +1 -1
- package/public/assets/{pieDiagram-SKSYHLDU-C8vfomtz.js → pieDiagram-SKSYHLDU-wxt-R3l5.js} +1 -1
- package/public/assets/{quadrantDiagram-337W2JSQ-DXT_qKk-.js → quadrantDiagram-337W2JSQ-0yTHkNo0.js} +1 -1
- package/public/assets/{requirementDiagram-Z7DCOOCP-Dj2MzFq3.js → requirementDiagram-Z7DCOOCP-CLqLwKcJ.js} +1 -1
- package/public/assets/{sankeyDiagram-WA2Y5GQK-YfmbQXg2.js → sankeyDiagram-WA2Y5GQK-CV2OX87k.js} +1 -1
- package/public/assets/{sequenceDiagram-2WXFIKYE-Bp9hgUSv.js → sequenceDiagram-2WXFIKYE-DaQifS2p.js} +1 -1
- package/public/assets/{stateDiagram-RAJIS63D-D8VgKzZe.js → stateDiagram-RAJIS63D-Bi5e4H5H.js} +1 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-D2d2wuS-.js +1 -0
- package/public/assets/{timeline-definition-YZTLITO2-3ErXxqpK.js → timeline-definition-YZTLITO2-Bu0j_UbL.js} +1 -1
- package/public/assets/{treemap-KZPCXAKY-D3-uSz_K.js → treemap-KZPCXAKY-BreHb2Q6.js} +1 -1
- package/public/assets/{vennDiagram-LZ73GAT5-D7Isk6A4.js → vennDiagram-LZ73GAT5-C5vHpUCv.js} +1 -1
- package/public/assets/{xychartDiagram-JWTSCODW-CpuvGwXM.js → xychartDiagram-JWTSCODW-DEN428FH.js} +1 -1
- package/public/index.html +2 -2
- package/public/sw.js +2 -2
- package/src/server/index.js +35 -4
- package/src/server/push.js +118 -0
- package/src/server/routes.js +350 -5
- package/src/server/sessions.js +144 -0
- package/src/server/websocket.js +21 -2
- package/src/utils/git.js +338 -1
- package/src/utils/update-check.js +139 -9
- package/src/utils/update-executor.js +340 -0
- package/src/utils/vapid.js +45 -0
- package/public/assets/channel-Bh_CZXn-.js +0 -1
- package/public/assets/classDiagram-VBA2DB6C-CSkcpaag.js +0 -1
- package/public/assets/classDiagram-v2-RAHNMMFH-CSkcpaag.js +0 -1
- package/public/assets/clone-BR1se3-G.js +0 -1
- package/public/assets/index-BWUfRdC9.js +0 -391
- package/public/assets/index-D_1GL6a5.css +0 -32
- package/public/assets/stateDiagram-v2-FVOUBMTO-By5luAVT.js +0 -1
package/src/utils/git.js
CHANGED
|
@@ -125,4 +125,341 @@ function parseStatus(output, ahead, behind) {
|
|
|
125
125
|
return { clean, modified, staged, untracked, ahead: ahead || 0, behind: behind || 0, summary };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
// --- Extended git utilities (async, using execFile for security) ---
|
|
129
|
+
|
|
130
|
+
const GIT_TIMEOUT = 5000;
|
|
131
|
+
const MAX_DIFF_BUFFER = 1024 * 1024; // 1 MB
|
|
132
|
+
const MAX_BLAME_BUFFER = 2 * 1024 * 1024; // 2 MB
|
|
133
|
+
const MAX_LOG_BUFFER = 1024 * 1024; // 1 MB
|
|
134
|
+
|
|
135
|
+
async function gitAsync(args, cwd, options = {}) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
require('child_process').execFile(
|
|
138
|
+
'git',
|
|
139
|
+
args,
|
|
140
|
+
{
|
|
141
|
+
cwd,
|
|
142
|
+
timeout: options.timeout || GIT_TIMEOUT,
|
|
143
|
+
maxBuffer: options.maxBuffer || MAX_DIFF_BUFFER,
|
|
144
|
+
},
|
|
145
|
+
(err, stdout) => {
|
|
146
|
+
if (err) return reject(err);
|
|
147
|
+
resolve(stdout);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function getDetailedStatus(cwd) {
|
|
154
|
+
try {
|
|
155
|
+
await gitAsync(['rev-parse', '--is-inside-work-tree'], cwd);
|
|
156
|
+
} catch {
|
|
157
|
+
return { isGitRepo: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const result = {
|
|
161
|
+
branch: null,
|
|
162
|
+
ahead: 0,
|
|
163
|
+
behind: 0,
|
|
164
|
+
staged: [],
|
|
165
|
+
modified: [],
|
|
166
|
+
untracked: [],
|
|
167
|
+
isGitRepo: true,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const raw = await gitAsync(['status', '--porcelain=v1', '-b'], cwd);
|
|
172
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
173
|
+
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
// Branch header line
|
|
176
|
+
if (line.startsWith('## ')) {
|
|
177
|
+
const branchInfo = line.slice(3);
|
|
178
|
+
const trackMatch = branchInfo.match(/^(.+?)(?:\.\.\.(.+?))?(?:\s+\[(.+)\])?$/);
|
|
179
|
+
if (trackMatch) {
|
|
180
|
+
result.branch = trackMatch[1];
|
|
181
|
+
const tracking = trackMatch[3];
|
|
182
|
+
if (tracking) {
|
|
183
|
+
const aheadMatch = tracking.match(/ahead (\d+)/);
|
|
184
|
+
const behindMatch = tracking.match(/behind (\d+)/);
|
|
185
|
+
if (aheadMatch) result.ahead = parseInt(aheadMatch[1], 10);
|
|
186
|
+
if (behindMatch) result.behind = parseInt(behindMatch[1], 10);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const index = line[0];
|
|
193
|
+
const working = line[1];
|
|
194
|
+
const filePart = line.slice(3);
|
|
195
|
+
|
|
196
|
+
// Untracked
|
|
197
|
+
if (index === '?' && working === '?') {
|
|
198
|
+
result.untracked.push(filePart);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Staged changes (index column)
|
|
203
|
+
if (index !== ' ' && index !== '?') {
|
|
204
|
+
const entry = { path: filePart, status: index, oldPath: null };
|
|
205
|
+
if (index === 'R' || index === 'C') {
|
|
206
|
+
const parts = filePart.split(' -> ');
|
|
207
|
+
if (parts.length === 2) {
|
|
208
|
+
entry.oldPath = parts[0];
|
|
209
|
+
entry.path = parts[1];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
result.staged.push(entry);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Working tree changes (working column)
|
|
216
|
+
if (working !== ' ' && working !== '?') {
|
|
217
|
+
result.modified.push({ path: filePart, status: working, oldPath: null });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.warn(`getDetailedStatus failed: ${err.message}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function parseDiffOutput(raw, filePath) {
|
|
228
|
+
const result = {
|
|
229
|
+
file: filePath,
|
|
230
|
+
hunks: [],
|
|
231
|
+
additions: 0,
|
|
232
|
+
deletions: 0,
|
|
233
|
+
isBinary: false,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (!raw.trim()) return result;
|
|
237
|
+
|
|
238
|
+
// Binary file detection — only check the diff header (lines before the first hunk).
|
|
239
|
+
// Content lines (prefixed with +/-/ ) may contain "Binary files ... differ" as text.
|
|
240
|
+
const firstHunkIdx = raw.indexOf('\n@@');
|
|
241
|
+
const header = firstHunkIdx >= 0 ? raw.slice(0, firstHunkIdx) : raw;
|
|
242
|
+
if (header.includes('Binary files') && header.includes('differ')) {
|
|
243
|
+
result.isBinary = true;
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Parse unified diff into hunks
|
|
248
|
+
const lines = raw.split('\n');
|
|
249
|
+
let currentHunk = null;
|
|
250
|
+
let oldLine = 0;
|
|
251
|
+
let newLine = 0;
|
|
252
|
+
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
// Hunk header
|
|
255
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
256
|
+
if (hunkMatch) {
|
|
257
|
+
currentHunk = {
|
|
258
|
+
header: line.match(/^@@.*@@/)[0],
|
|
259
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
260
|
+
oldLines: parseInt(hunkMatch[2] ?? '1', 10),
|
|
261
|
+
newStart: parseInt(hunkMatch[3], 10),
|
|
262
|
+
newLines: parseInt(hunkMatch[4] ?? '1', 10),
|
|
263
|
+
lines: [],
|
|
264
|
+
};
|
|
265
|
+
result.hunks.push(currentHunk);
|
|
266
|
+
oldLine = currentHunk.oldStart;
|
|
267
|
+
newLine = currentHunk.newStart;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!currentHunk) continue;
|
|
272
|
+
|
|
273
|
+
if (line.startsWith('+')) {
|
|
274
|
+
currentHunk.lines.push({
|
|
275
|
+
type: 'add',
|
|
276
|
+
content: line.slice(1),
|
|
277
|
+
oldLine: null,
|
|
278
|
+
newLine: newLine++,
|
|
279
|
+
});
|
|
280
|
+
result.additions++;
|
|
281
|
+
} else if (line.startsWith('-')) {
|
|
282
|
+
currentHunk.lines.push({
|
|
283
|
+
type: 'remove',
|
|
284
|
+
content: line.slice(1),
|
|
285
|
+
oldLine: oldLine++,
|
|
286
|
+
newLine: null,
|
|
287
|
+
});
|
|
288
|
+
result.deletions++;
|
|
289
|
+
} else if (line.startsWith(' ')) {
|
|
290
|
+
currentHunk.lines.push({
|
|
291
|
+
type: 'context',
|
|
292
|
+
content: line.slice(1),
|
|
293
|
+
oldLine: oldLine++,
|
|
294
|
+
newLine: newLine++,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Skip diff header lines (diff --git, index, ---, +++)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function getFileDiff(cwd, filePath, options = {}) {
|
|
304
|
+
const { staged = false, untracked = false, context = 3 } = options;
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Untracked files: use --no-index to diff against the null device
|
|
308
|
+
const nullDevice = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
309
|
+
if (untracked) {
|
|
310
|
+
const raw = await new Promise((resolve, reject) => {
|
|
311
|
+
require('child_process').execFile(
|
|
312
|
+
'git',
|
|
313
|
+
['diff', '--no-index', '--no-color', `--unified=${context}`, '--', nullDevice, filePath],
|
|
314
|
+
{
|
|
315
|
+
cwd,
|
|
316
|
+
timeout: GIT_TIMEOUT,
|
|
317
|
+
maxBuffer: MAX_DIFF_BUFFER,
|
|
318
|
+
},
|
|
319
|
+
(err, stdout) => {
|
|
320
|
+
// git diff --no-index exits with 1 when files differ — that's expected
|
|
321
|
+
if (err && err.code !== 1) return reject(err);
|
|
322
|
+
resolve(stdout || '');
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
return parseDiffOutput(raw, filePath);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const args = ['diff', `--unified=${context}`, '--no-color'];
|
|
330
|
+
if (staged) args.push('--cached');
|
|
331
|
+
args.push('--', filePath);
|
|
332
|
+
|
|
333
|
+
const raw = await gitAsync(args, cwd, { maxBuffer: MAX_DIFF_BUFFER });
|
|
334
|
+
return parseDiffOutput(raw, filePath);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
// Empty diff or git error
|
|
337
|
+
if (err.code !== 1) {
|
|
338
|
+
log.warn(`getFileDiff failed: ${err.message}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
file: filePath,
|
|
344
|
+
hunks: [],
|
|
345
|
+
additions: 0,
|
|
346
|
+
deletions: 0,
|
|
347
|
+
isBinary: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function getFileBlame(cwd, filePath) {
|
|
352
|
+
const result = { file: filePath, lines: [] };
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const raw = await gitAsync(['blame', '--porcelain', '--', filePath], cwd, {
|
|
356
|
+
maxBuffer: MAX_BLAME_BUFFER,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const rawLines = raw.split('\n');
|
|
360
|
+
let currentCommit = null;
|
|
361
|
+
let currentLine = null;
|
|
362
|
+
const commitInfo = {}; // cache commit metadata
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
365
|
+
const line = rawLines[i];
|
|
366
|
+
|
|
367
|
+
// Commit line: <40-char-hash> <orig-line> <final-line> [<num-lines>]
|
|
368
|
+
const commitMatch = line.match(/^([0-9a-f]{40})\s+(\d+)\s+(\d+)(?:\s+(\d+))?$/);
|
|
369
|
+
if (commitMatch) {
|
|
370
|
+
currentCommit = commitMatch[1];
|
|
371
|
+
currentLine = parseInt(commitMatch[3], 10);
|
|
372
|
+
|
|
373
|
+
if (!commitInfo[currentCommit]) {
|
|
374
|
+
commitInfo[currentCommit] = {
|
|
375
|
+
author: null,
|
|
376
|
+
date: null,
|
|
377
|
+
summary: null,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Metadata lines
|
|
384
|
+
if (currentCommit && line.startsWith('author ')) {
|
|
385
|
+
commitInfo[currentCommit].author = line.slice(7);
|
|
386
|
+
} else if (currentCommit && line.startsWith('author-time ')) {
|
|
387
|
+
const timestamp = parseInt(line.slice(12), 10);
|
|
388
|
+
commitInfo[currentCommit].date = new Date(timestamp * 1000).toISOString();
|
|
389
|
+
} else if (currentCommit && line.startsWith('summary ')) {
|
|
390
|
+
commitInfo[currentCommit].summary = line.slice(8);
|
|
391
|
+
} else if (currentCommit && line.startsWith('\t')) {
|
|
392
|
+
// Content line
|
|
393
|
+
const info = commitInfo[currentCommit];
|
|
394
|
+
const isUncommitted = currentCommit === '0000000000000000000000000000000000000000';
|
|
395
|
+
result.lines.push({
|
|
396
|
+
line: currentLine,
|
|
397
|
+
content: line.slice(1),
|
|
398
|
+
commit: isUncommitted ? null : currentCommit.slice(0, 7),
|
|
399
|
+
author: isUncommitted ? 'Not Committed Yet' : info.author || 'Unknown',
|
|
400
|
+
date: isUncommitted ? null : info.date || null,
|
|
401
|
+
summary: isUncommitted ? 'Uncommitted changes' : info.summary || '',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
log.warn(`getFileBlame failed: ${err.message}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const LOG_SEPARATOR = '---GIT_LOG_SEP---';
|
|
413
|
+
const LOG_FIELD_SEP = '---GIT_FIELD_SEP---';
|
|
414
|
+
const LOG_FORMAT = [
|
|
415
|
+
'%H', // hash
|
|
416
|
+
'%h', // short hash
|
|
417
|
+
'%an', // author name
|
|
418
|
+
'%ae', // author email
|
|
419
|
+
'%aI', // author date ISO
|
|
420
|
+
'%s', // subject
|
|
421
|
+
'%b', // body
|
|
422
|
+
].join(LOG_FIELD_SEP);
|
|
423
|
+
|
|
424
|
+
async function getGitLog(cwd, options = {}) {
|
|
425
|
+
const limit = Math.min(Math.max(parseInt(options.limit, 10) || 20, 1), 100);
|
|
426
|
+
const result = { commits: [] };
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const args = ['log', `--format=${LOG_SEPARATOR}${LOG_FORMAT}`, `-n`, String(limit)];
|
|
430
|
+
if (options.file) {
|
|
431
|
+
args.push('--follow', '--', options.file);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const raw = await gitAsync(args, cwd, { maxBuffer: MAX_LOG_BUFFER });
|
|
435
|
+
|
|
436
|
+
const entries = raw.split(LOG_SEPARATOR).filter((e) => e.trim());
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
const fields = entry.trim().split(LOG_FIELD_SEP);
|
|
439
|
+
if (fields.length < 6) continue;
|
|
440
|
+
result.commits.push({
|
|
441
|
+
hash: fields[0],
|
|
442
|
+
shortHash: fields[1],
|
|
443
|
+
author: fields[2],
|
|
444
|
+
email: fields[3],
|
|
445
|
+
date: fields[4],
|
|
446
|
+
subject: fields[5],
|
|
447
|
+
body: (fields[6] || '').trim(),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
log.warn(`getGitLog failed: ${err.message}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
module.exports = {
|
|
458
|
+
getGitInfo,
|
|
459
|
+
parseRemoteUrl,
|
|
460
|
+
parseStatus,
|
|
461
|
+
getDetailedStatus,
|
|
462
|
+
getFileDiff,
|
|
463
|
+
getFileBlame,
|
|
464
|
+
getGitLog,
|
|
465
|
+
};
|
|
@@ -73,6 +73,8 @@ function normalizeVersion(version) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Compare two semver version strings (e.g. "1.10.2" vs "1.11.0").
|
|
75
75
|
* Returns true if `latest` is newer than `current`.
|
|
76
|
+
* Pre-release versions (e.g. "1.15.3-rc.1") are considered older than
|
|
77
|
+
* the same stable version ("1.15.3"), so an update will be offered.
|
|
76
78
|
* Returns false if either version cannot be parsed.
|
|
77
79
|
*/
|
|
78
80
|
function isNewerVersion(current, latest) {
|
|
@@ -83,9 +85,25 @@ function isNewerVersion(current, latest) {
|
|
|
83
85
|
if (lat[i] > cur[i]) return true;
|
|
84
86
|
if (lat[i] < cur[i]) return false;
|
|
85
87
|
}
|
|
88
|
+
// Same base version — if current is a pre-release but latest is stable,
|
|
89
|
+
// the stable release is newer (e.g. 1.15.3-rc.1 → 1.15.3)
|
|
90
|
+
if (isPreRelease(current) && !isPreRelease(latest)) return true;
|
|
86
91
|
return false;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Check if a version string contains pre-release metadata (e.g. "-rc.1", "-dev.5").
|
|
96
|
+
*/
|
|
97
|
+
function isPreRelease(version) {
|
|
98
|
+
if (typeof version !== 'string') return false;
|
|
99
|
+
let v = version.trim();
|
|
100
|
+
if (v[0] === 'v' || v[0] === 'V') v = v.slice(1);
|
|
101
|
+
// Strip build metadata first (+foo)
|
|
102
|
+
const plusIdx = v.indexOf('+');
|
|
103
|
+
if (plusIdx !== -1) v = v.slice(0, plusIdx);
|
|
104
|
+
return v.includes('-');
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
/**
|
|
90
108
|
* Strip ANSI escape sequences and control characters from a string.
|
|
91
109
|
* Prevents terminal injection if the registry returns malicious data.
|
|
@@ -222,39 +240,151 @@ async function checkForUpdate({ currentVersion, force = false } = {}) {
|
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
/**
|
|
225
|
-
* Detect how TermBeam was installed and return the appropriate update command
|
|
226
|
-
*
|
|
243
|
+
* Detect how TermBeam was installed and return the appropriate update command,
|
|
244
|
+
* whether it can auto-update, and the restart strategy.
|
|
245
|
+
* @returns {{ method: string, command: string, canAutoUpdate: boolean, restartStrategy: 'pm2'|'exit'|'none', installCmd: string|null, installArgs: string[]|null }}
|
|
246
|
+
* Note: installCmd/installArgs are internal — stripped before sending to API clients.
|
|
227
247
|
*/
|
|
228
248
|
function detectInstallMethod() {
|
|
229
249
|
// npx / npm exec — npm sets npm_command=exec
|
|
230
250
|
if (process.env.npm_command === 'exec') {
|
|
231
251
|
log.debug('Install method: npx');
|
|
232
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
method: 'npx',
|
|
254
|
+
command: 'npx termbeam@latest',
|
|
255
|
+
installCmd: 'npx',
|
|
256
|
+
installArgs: ['termbeam@latest'],
|
|
257
|
+
canAutoUpdate: false,
|
|
258
|
+
restartStrategy: 'none',
|
|
259
|
+
};
|
|
233
260
|
}
|
|
234
261
|
|
|
262
|
+
// PM2 managed — detect via PM2 environment variables
|
|
263
|
+
const isPm2 = isRunningUnderPm2();
|
|
264
|
+
|
|
235
265
|
// Detect package manager from npm_execpath (set during npm/yarn/pnpm lifecycle)
|
|
266
|
+
// Check this before file-system checks since env vars are more reliable
|
|
236
267
|
const execPath = process.env.npm_execpath || '';
|
|
237
268
|
if (execPath.includes('yarn')) {
|
|
238
|
-
log.debug(
|
|
239
|
-
return {
|
|
269
|
+
log.debug(`Install method: yarn${isPm2 ? ' (PM2)' : ''}`);
|
|
270
|
+
return {
|
|
271
|
+
method: 'yarn',
|
|
272
|
+
command: 'yarn global add termbeam@latest',
|
|
273
|
+
installCmd: 'yarn',
|
|
274
|
+
installArgs: ['global', 'add', 'termbeam@latest'],
|
|
275
|
+
canAutoUpdate: true,
|
|
276
|
+
restartStrategy: isPm2 ? 'pm2' : 'exit',
|
|
277
|
+
};
|
|
240
278
|
}
|
|
241
279
|
if (execPath.includes('pnpm')) {
|
|
242
|
-
log.debug(
|
|
243
|
-
return {
|
|
280
|
+
log.debug(`Install method: pnpm${isPm2 ? ' (PM2)' : ''}`);
|
|
281
|
+
return {
|
|
282
|
+
method: 'pnpm',
|
|
283
|
+
command: 'pnpm add -g termbeam@latest',
|
|
284
|
+
installCmd: 'pnpm',
|
|
285
|
+
installArgs: ['add', '-g', 'termbeam@latest'],
|
|
286
|
+
canAutoUpdate: true,
|
|
287
|
+
restartStrategy: isPm2 ? 'pm2' : 'exit',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Development / git clone — not in node_modules and .git exists
|
|
292
|
+
// Check before Docker: a git checkout running inside a container (CI/devcontainers)
|
|
293
|
+
// should be treated as source, not Docker
|
|
294
|
+
if (isRunningFromSource()) {
|
|
295
|
+
log.debug('Install method: source');
|
|
296
|
+
return {
|
|
297
|
+
method: 'source',
|
|
298
|
+
command: 'git pull && npm install && npm run build:frontend',
|
|
299
|
+
installCmd: null,
|
|
300
|
+
installArgs: null,
|
|
301
|
+
canAutoUpdate: false,
|
|
302
|
+
restartStrategy: 'none',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Docker — check for /.dockerenv or /proc/1/cgroup containing docker
|
|
307
|
+
if (isRunningInDocker()) {
|
|
308
|
+
log.debug('Install method: docker');
|
|
309
|
+
return {
|
|
310
|
+
method: 'docker',
|
|
311
|
+
command: 'docker pull termbeam:latest && docker-compose up -d',
|
|
312
|
+
installCmd: null,
|
|
313
|
+
installArgs: null,
|
|
314
|
+
canAutoUpdate: false,
|
|
315
|
+
restartStrategy: 'none',
|
|
316
|
+
};
|
|
244
317
|
}
|
|
245
318
|
|
|
246
319
|
// Default: npm global install
|
|
247
|
-
log.debug(
|
|
248
|
-
return {
|
|
320
|
+
log.debug(`Install method: npm${isPm2 ? ' (PM2)' : ''}`);
|
|
321
|
+
return {
|
|
322
|
+
method: 'npm',
|
|
323
|
+
command: 'npm install -g termbeam@latest',
|
|
324
|
+
installCmd: 'npm',
|
|
325
|
+
installArgs: ['install', '-g', 'termbeam@latest'],
|
|
326
|
+
canAutoUpdate: true,
|
|
327
|
+
restartStrategy: isPm2 ? 'pm2' : 'exit',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Detect if running inside a Docker container.
|
|
333
|
+
*/
|
|
334
|
+
function isRunningInDocker() {
|
|
335
|
+
try {
|
|
336
|
+
if (fs.existsSync('/.dockerenv')) return true;
|
|
337
|
+
} catch {
|
|
338
|
+
// ignore
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
|
|
342
|
+
if (cgroup.includes('docker') || cgroup.includes('containerd')) return true;
|
|
343
|
+
} catch {
|
|
344
|
+
// Not Linux or no access — not Docker
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Detect if running from a git source checkout (not installed as a package).
|
|
351
|
+
* Walks upward from __dirname looking for .git to avoid fragile fixed-depth assumptions.
|
|
352
|
+
*/
|
|
353
|
+
function isRunningFromSource() {
|
|
354
|
+
// If __dirname is inside node_modules, it's a package install
|
|
355
|
+
if (__dirname.includes('node_modules')) return false;
|
|
356
|
+
try {
|
|
357
|
+
let currentDir = __dirname;
|
|
358
|
+
for (let i = 0; i < 10; i++) {
|
|
359
|
+
if (fs.existsSync(path.join(currentDir, '.git'))) return true;
|
|
360
|
+
const parentDir = path.dirname(currentDir);
|
|
361
|
+
if (!parentDir || parentDir === currentDir) break;
|
|
362
|
+
currentDir = parentDir;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Detect if running under PM2 process manager.
|
|
372
|
+
*/
|
|
373
|
+
function isRunningUnderPm2() {
|
|
374
|
+
return !!(process.env.PM2_HOME || process.env.pm_id || process.env.PM2_USAGE);
|
|
249
375
|
}
|
|
250
376
|
|
|
251
377
|
module.exports = {
|
|
252
378
|
checkForUpdate,
|
|
253
379
|
isNewerVersion,
|
|
380
|
+
isPreRelease,
|
|
254
381
|
normalizeVersion,
|
|
255
382
|
fetchLatestVersion,
|
|
256
383
|
readCache,
|
|
257
384
|
writeCache,
|
|
258
385
|
sanitizeVersion,
|
|
259
386
|
detectInstallMethod,
|
|
387
|
+
isRunningInDocker,
|
|
388
|
+
isRunningFromSource,
|
|
389
|
+
isRunningUnderPm2,
|
|
260
390
|
};
|