termbeam 1.12.5 → 1.13.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/package.json +1 -1
- package/public/assets/index-BoTisaNy.js +108 -0
- package/public/assets/index-yPsf11Zp.css +32 -0
- package/public/index.html +2 -2
- package/public/sw.js +2 -2
- package/src/cli/client.js +9 -0
- package/src/cli/index.js +11 -1
- package/src/cli/interactive.js +7 -0
- package/src/cli/resume.js +9 -0
- package/src/cli/service.js +10 -0
- package/src/server/auth.js +2 -0
- package/src/server/index.js +6 -0
- package/src/server/preview.js +2 -0
- package/src/server/routes.js +25 -2
- package/src/server/sessions.js +26 -4
- package/src/server/websocket.js +2 -0
- package/src/utils/git.js +3 -0
- package/src/utils/update-check.js +22 -2
- package/src/utils/version.js +23 -4
- package/public/assets/index-DhjvNUWH.js +0 -108
- package/public/assets/index-Lhj5V2mZ.css +0 -32
package/src/server/auth.js
CHANGED
|
@@ -294,6 +294,7 @@ function createAuth(password) {
|
|
|
294
294
|
// Periodically clean up expired tokens and stale rate-limit entries
|
|
295
295
|
const cleanupInterval = setInterval(
|
|
296
296
|
() => {
|
|
297
|
+
log.debug('Token cleanup: removing expired auth and share tokens');
|
|
297
298
|
const now = Date.now();
|
|
298
299
|
for (const [token, expiry] of tokens) {
|
|
299
300
|
if (now > expiry) tokens.delete(token);
|
|
@@ -339,6 +340,7 @@ function createAuth(password) {
|
|
|
339
340
|
function generateToken() {
|
|
340
341
|
const token = crypto.randomBytes(32).toString('hex');
|
|
341
342
|
tokens.set(token, Date.now() + 24 * 60 * 60 * 1000);
|
|
343
|
+
log.debug('Auth token generated (24h expiry)');
|
|
342
344
|
return token;
|
|
343
345
|
}
|
|
344
346
|
|
package/src/server/index.js
CHANGED
|
@@ -84,6 +84,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
84
84
|
function shutdown() {
|
|
85
85
|
if (shuttingDown) return;
|
|
86
86
|
shuttingDown = true;
|
|
87
|
+
log.info('Shutdown initiated');
|
|
87
88
|
auth.cleanup();
|
|
88
89
|
sessions.shutdown();
|
|
89
90
|
cleanupUploadedFiles();
|
|
@@ -231,6 +232,7 @@ function createTermBeamServer(overrides = {}) {
|
|
|
231
232
|
publicUrl = tunnel.url;
|
|
232
233
|
state.shareBaseUrl = publicUrl;
|
|
233
234
|
} else {
|
|
235
|
+
log.warn('Tunnel failed to start, falling back to LAN-only');
|
|
234
236
|
console.log(' ⚠️ Tunnel failed to start. Using LAN only.');
|
|
235
237
|
}
|
|
236
238
|
}
|
|
@@ -322,18 +324,22 @@ module.exports = { createTermBeamServer, getLocalIP };
|
|
|
322
324
|
// Auto-start when run directly (e.g. `node src/server.js`)
|
|
323
325
|
if (require.main === module) {
|
|
324
326
|
const instance = createTermBeamServer();
|
|
327
|
+
const log = require('../utils/logger');
|
|
325
328
|
|
|
326
329
|
process.on('SIGINT', () => {
|
|
330
|
+
log.info('Received SIGINT signal');
|
|
327
331
|
console.log('\n[termbeam] Shutting down...');
|
|
328
332
|
instance.shutdown();
|
|
329
333
|
setTimeout(() => process.exit(0), 500).unref();
|
|
330
334
|
});
|
|
331
335
|
process.on('SIGTERM', () => {
|
|
336
|
+
log.info('Received SIGTERM signal');
|
|
332
337
|
console.log('\n[termbeam] Shutting down...');
|
|
333
338
|
instance.shutdown();
|
|
334
339
|
setTimeout(() => process.exit(0), 500).unref();
|
|
335
340
|
});
|
|
336
341
|
process.on('uncaughtException', (err) => {
|
|
342
|
+
log.error(`Uncaught exception: ${err.message}`);
|
|
337
343
|
console.error('[termbeam] Uncaught exception:', err.message);
|
|
338
344
|
cleanupTunnel();
|
|
339
345
|
process.exit(1);
|
package/src/server/preview.js
CHANGED
|
@@ -26,6 +26,7 @@ function createPreviewProxy() {
|
|
|
26
26
|
function proxyRequest(req, res) {
|
|
27
27
|
const port = Number(req.params.port);
|
|
28
28
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
29
|
+
log.warn(`Preview proxy: invalid port ${req.params.port} rejected`);
|
|
29
30
|
return res
|
|
30
31
|
.status(400)
|
|
31
32
|
.json({ error: 'Invalid port: must be an integer between 1 and 65535' });
|
|
@@ -87,6 +88,7 @@ function createPreviewProxy() {
|
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
proxyReq.setTimeout(PROXY_TIMEOUT, () => {
|
|
91
|
+
log.warn(`Preview proxy: request to port ${port} timed out after ${PROXY_TIMEOUT}ms`);
|
|
90
92
|
proxyReq.destroy();
|
|
91
93
|
if (!res.headersSent) {
|
|
92
94
|
res.status(504).json({ error: 'Gateway timeout: upstream server did not respond in time' });
|
package/src/server/routes.js
CHANGED
|
@@ -83,12 +83,14 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
83
83
|
|
|
84
84
|
// Version API
|
|
85
85
|
app.get('/api/version', (_req, res) => {
|
|
86
|
+
log.debug('Version requested');
|
|
86
87
|
const { getVersion } = require('../utils/version');
|
|
87
88
|
res.json({ version: getVersion() });
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
// Update check API
|
|
91
92
|
app.get('/api/update-check', apiRateLimit, auth.middleware, async (req, res) => {
|
|
93
|
+
log.debug('Update check requested');
|
|
92
94
|
const { checkForUpdate, detectInstallMethod } = require('../utils/update-check');
|
|
93
95
|
const force = req.query.force === 'true';
|
|
94
96
|
|
|
@@ -97,7 +99,8 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
97
99
|
const installInfo = detectInstallMethod();
|
|
98
100
|
state.updateInfo = { ...info, ...installInfo };
|
|
99
101
|
res.json(state.updateInfo);
|
|
100
|
-
} catch {
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.warn(`Update check failed: ${err.message}`);
|
|
101
104
|
const installInfo = detectInstallMethod();
|
|
102
105
|
const fallback = {
|
|
103
106
|
current: config.version,
|
|
@@ -144,6 +147,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
144
147
|
|
|
145
148
|
// Share token — generates a temporary share token for the share button
|
|
146
149
|
app.get('/api/share-token', auth.middleware, (req, res) => {
|
|
150
|
+
log.debug('Share token requested');
|
|
147
151
|
if (!auth.password) return res.status(404).json({ error: 'auth disabled' });
|
|
148
152
|
const shareToken = auth.generateShareToken();
|
|
149
153
|
const base = (state && state.shareBaseUrl) || `${req.protocol}://${req.get('host')}`;
|
|
@@ -152,6 +156,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
152
156
|
|
|
153
157
|
// Session API
|
|
154
158
|
app.get('/api/sessions', apiRateLimit, auth.middleware, (_req, res) => {
|
|
159
|
+
log.debug('Sessions list requested');
|
|
155
160
|
res.json(sessions.list());
|
|
156
161
|
});
|
|
157
162
|
|
|
@@ -163,6 +168,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
163
168
|
const availableShells = detectShells();
|
|
164
169
|
const isValid = availableShells.some((s) => s.path === shell || s.cmd === shell);
|
|
165
170
|
if (!isValid) {
|
|
171
|
+
log.warn(`Session creation failed: invalid shell "${shell}"`);
|
|
166
172
|
return res.status(400).json({ error: 'Invalid shell' });
|
|
167
173
|
}
|
|
168
174
|
}
|
|
@@ -170,6 +176,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
170
176
|
// Validate args field — must be an array of strings
|
|
171
177
|
if (shellArgs !== undefined) {
|
|
172
178
|
if (!Array.isArray(shellArgs) || !shellArgs.every((a) => typeof a === 'string')) {
|
|
179
|
+
log.warn('Session creation failed: args must be an array of strings');
|
|
173
180
|
return res.status(400).json({ error: 'args must be an array of strings' });
|
|
174
181
|
}
|
|
175
182
|
}
|
|
@@ -177,6 +184,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
177
184
|
// Validate initialCommand field — must be a string
|
|
178
185
|
if (initialCommand !== undefined && initialCommand !== null) {
|
|
179
186
|
if (typeof initialCommand !== 'string') {
|
|
187
|
+
log.warn('Session creation failed: initialCommand must be a string');
|
|
180
188
|
return res.status(400).json({ error: 'initialCommand must be a string' });
|
|
181
189
|
}
|
|
182
190
|
}
|
|
@@ -184,13 +192,16 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
184
192
|
// Validate cwd field
|
|
185
193
|
if (cwd) {
|
|
186
194
|
if (!path.isAbsolute(cwd)) {
|
|
195
|
+
log.warn(`Session creation failed: cwd must be an absolute path (got "${cwd}")`);
|
|
187
196
|
return res.status(400).json({ error: 'cwd must be an absolute path' });
|
|
188
197
|
}
|
|
189
198
|
try {
|
|
190
199
|
if (!fs.statSync(cwd).isDirectory()) {
|
|
200
|
+
log.warn(`Session creation failed: cwd is not a directory (${cwd})`);
|
|
191
201
|
return res.status(400).json({ error: 'cwd is not a directory' });
|
|
192
202
|
}
|
|
193
203
|
} catch {
|
|
204
|
+
log.warn(`Session creation failed: cwd does not exist (${cwd})`);
|
|
194
205
|
return res.status(400).json({ error: 'cwd does not exist' });
|
|
195
206
|
}
|
|
196
207
|
}
|
|
@@ -216,6 +227,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
216
227
|
|
|
217
228
|
// Available shells
|
|
218
229
|
app.get('/api/shells', auth.middleware, (_req, res) => {
|
|
230
|
+
log.debug('Available shells requested');
|
|
219
231
|
const shells = detectShells();
|
|
220
232
|
const ds = config.defaultShell;
|
|
221
233
|
const match = shells.find((s) => s.cmd === ds || s.path === ds || s.name === ds);
|
|
@@ -223,6 +235,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
223
235
|
});
|
|
224
236
|
|
|
225
237
|
app.get('/api/sessions/:id/detect-port', auth.middleware, (req, res) => {
|
|
238
|
+
log.debug(`Port detection requested for session ${req.params.id}`);
|
|
226
239
|
const session = sessions.get(req.params.id);
|
|
227
240
|
if (!session) return res.status(404).json({ error: 'not found' });
|
|
228
241
|
|
|
@@ -236,6 +249,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
if (lastPort !== null) {
|
|
252
|
+
log.debug(`Port detected for session ${req.params.id}: ${lastPort}`);
|
|
239
253
|
res.json({ detected: true, port: lastPort });
|
|
240
254
|
} else {
|
|
241
255
|
res.json({ detected: false });
|
|
@@ -244,8 +258,10 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
244
258
|
|
|
245
259
|
app.delete('/api/sessions/:id', auth.middleware, (req, res) => {
|
|
246
260
|
if (sessions.delete(req.params.id)) {
|
|
261
|
+
log.info(`Session deleted: ${req.params.id}`);
|
|
247
262
|
res.status(204).end();
|
|
248
263
|
} else {
|
|
264
|
+
log.warn(`Session delete failed: not found (${req.params.id})`);
|
|
249
265
|
res.status(404).json({ error: 'not found' });
|
|
250
266
|
}
|
|
251
267
|
});
|
|
@@ -256,14 +272,17 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
256
272
|
if (color !== undefined) updates.color = color;
|
|
257
273
|
if (name !== undefined) updates.name = name;
|
|
258
274
|
if (sessions.update(req.params.id, updates)) {
|
|
275
|
+
log.info(`Session updated: ${req.params.id}`);
|
|
259
276
|
res.json({ ok: true });
|
|
260
277
|
} else {
|
|
278
|
+
log.warn(`Session update failed: not found (${req.params.id})`);
|
|
261
279
|
res.status(404).json({ error: 'not found' });
|
|
262
280
|
}
|
|
263
281
|
});
|
|
264
282
|
|
|
265
283
|
// Image upload
|
|
266
284
|
app.post('/api/upload', auth.middleware, (req, res) => {
|
|
285
|
+
log.debug('Image upload started');
|
|
267
286
|
const contentType = req.headers['content-type'] || '';
|
|
268
287
|
if (!contentType.startsWith('image/')) {
|
|
269
288
|
log.warn(`Upload rejected: invalid content-type "${contentType}"`);
|
|
@@ -334,6 +353,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
334
353
|
|
|
335
354
|
// General file upload to a session's working directory
|
|
336
355
|
app.post('/api/sessions/:id/upload', apiRateLimit, auth.middleware, (req, res) => {
|
|
356
|
+
log.debug(`File upload started for session ${req.params.id}`);
|
|
337
357
|
const session = sessions.get(req.params.id);
|
|
338
358
|
if (!session) {
|
|
339
359
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -440,6 +460,7 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
440
460
|
|
|
441
461
|
// Directory listing for folder browser
|
|
442
462
|
app.get('/api/dirs', apiRateLimit, auth.middleware, (req, res) => {
|
|
463
|
+
log.debug(`Directory listing requested: ${req.query.q || config.cwd}`);
|
|
443
464
|
const query = req.query.q || config.cwd + path.sep;
|
|
444
465
|
const endsWithSep = query.endsWith('/') || query.endsWith('\\');
|
|
445
466
|
const dir = path.resolve(endsWithSep ? query : path.dirname(query));
|
|
@@ -453,13 +474,15 @@ function setupRoutes(app, { auth, sessions, config, state }) {
|
|
|
453
474
|
.slice(0, 50)
|
|
454
475
|
.map((e) => path.join(dir, e.name));
|
|
455
476
|
res.json({ base: dir, dirs });
|
|
456
|
-
} catch {
|
|
477
|
+
} catch (err) {
|
|
478
|
+
log.warn(`Directory listing failed: ${err.message}`);
|
|
457
479
|
res.json({ base: dir, dirs: [] });
|
|
458
480
|
}
|
|
459
481
|
});
|
|
460
482
|
}
|
|
461
483
|
|
|
462
484
|
function cleanupUploadedFiles() {
|
|
485
|
+
log.debug(`Cleaning up ${uploadedFiles.size} uploaded files`);
|
|
463
486
|
for (const [_id, filepath] of uploadedFiles) {
|
|
464
487
|
try {
|
|
465
488
|
if (fs.existsSync(filepath)) {
|
package/src/server/sessions.js
CHANGED
|
@@ -15,17 +15,20 @@ function getCachedGitInfo(sessionId, pid, originalCwd) {
|
|
|
15
15
|
const now = Date.now();
|
|
16
16
|
const cached = _gitCache.get(sessionId);
|
|
17
17
|
if (cached && now - cached.ts < GIT_CACHE_TTL) {
|
|
18
|
+
log.debug(`Git cache hit for session ${sessionId}`);
|
|
18
19
|
return { cwd: cached.cwd, git: cached.git };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Always refresh asynchronously to avoid blocking the event loop.
|
|
22
23
|
// Return stale data if available, or null on first call.
|
|
24
|
+
log.debug(`Git cache miss for session ${sessionId}, scheduling refresh`);
|
|
23
25
|
scheduleGitRefresh(sessionId, pid, originalCwd);
|
|
24
26
|
if (cached) return { cwd: cached.cwd, git: cached.git };
|
|
25
27
|
return { cwd: originalCwd, git: null };
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function scheduleGitRefresh(sessionId, pid, originalCwd) {
|
|
31
|
+
log.debug(`Scheduling git refresh for session ${sessionId}`);
|
|
29
32
|
// Mark as refreshing to prevent duplicate refreshes
|
|
30
33
|
const cached = _gitCache.get(sessionId);
|
|
31
34
|
if (cached && cached._refreshing) return;
|
|
@@ -61,6 +64,7 @@ function scheduleGitRefresh(sessionId, pid, originalCwd) {
|
|
|
61
64
|
}
|
|
62
65
|
const git = getGitInfo(liveCwd);
|
|
63
66
|
_gitCache.set(sessionId, { cwd: liveCwd, git, ts: Date.now() });
|
|
67
|
+
log.debug(`Git refresh complete for session ${sessionId} (cwd=${liveCwd})`);
|
|
64
68
|
});
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -97,14 +101,17 @@ class SessionManager {
|
|
|
97
101
|
/[;&|`$(){}\[\]!#~]/.test(shell) ||
|
|
98
102
|
(!path.isAbsolute(shell) && !shell.match(/^[a-zA-Z0-9._-]+(\.exe)?$/))
|
|
99
103
|
) {
|
|
104
|
+
log.warn(`Invalid shell rejected: ${shell}`);
|
|
100
105
|
throw new Error('Invalid shell');
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
// Defense-in-depth: validate args and initialCommand types
|
|
104
109
|
if (!Array.isArray(args) || !args.every((a) => typeof a === 'string')) {
|
|
110
|
+
log.warn(`Invalid args rejected: ${JSON.stringify(args)}`);
|
|
105
111
|
throw new Error('args must be an array of strings');
|
|
106
112
|
}
|
|
107
113
|
if (initialCommand !== null && typeof initialCommand !== 'string') {
|
|
114
|
+
log.warn(`Invalid initialCommand rejected: ${typeof initialCommand}`);
|
|
108
115
|
throw new Error('initialCommand must be a string');
|
|
109
116
|
}
|
|
110
117
|
|
|
@@ -112,6 +119,7 @@ class SessionManager {
|
|
|
112
119
|
if (!color) {
|
|
113
120
|
color = SESSION_COLORS[this.sessions.size % SESSION_COLORS.length];
|
|
114
121
|
}
|
|
122
|
+
log.debug(`Spawning PTY: shell=${shell}, args=[${args.length} items], cwd=${cwd}`);
|
|
115
123
|
const ptyProcess = pty.spawn(shell, args, {
|
|
116
124
|
name: 'xterm-256color',
|
|
117
125
|
cols,
|
|
@@ -122,6 +130,7 @@ class SessionManager {
|
|
|
122
130
|
|
|
123
131
|
// Send initial command once the shell is ready
|
|
124
132
|
if (initialCommand) {
|
|
133
|
+
log.debug(`Scheduling initialCommand for session ${id} (${initialCommand.length} chars)`);
|
|
125
134
|
setTimeout(() => ptyProcess.write(initialCommand + '\r'), 300);
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -177,6 +186,7 @@ class SessionManager {
|
|
|
177
186
|
}
|
|
178
187
|
// High/low water scrollback cap: trim to 500k chars when buffer exceeds 1,000,000 chars
|
|
179
188
|
if (session.scrollbackBuf.length > 1000000) {
|
|
189
|
+
log.debug(`Trimming scrollback buffer from ${session.scrollbackBuf.length} to 500k chars`);
|
|
180
190
|
let buf = session.scrollbackBuf.slice(-500000);
|
|
181
191
|
// Advance to first newline to avoid starting mid-line
|
|
182
192
|
const nlIdx = buf.indexOf('\n');
|
|
@@ -211,8 +221,18 @@ class SessionManager {
|
|
|
211
221
|
update(id, fields) {
|
|
212
222
|
const s = this.sessions.get(id);
|
|
213
223
|
if (!s) return false;
|
|
214
|
-
|
|
215
|
-
if (fields.
|
|
224
|
+
const changes = [];
|
|
225
|
+
if (fields.color !== undefined) {
|
|
226
|
+
s.color = fields.color;
|
|
227
|
+
changes.push(`color=${fields.color}`);
|
|
228
|
+
}
|
|
229
|
+
if (fields.name !== undefined) {
|
|
230
|
+
s.name = fields.name;
|
|
231
|
+
changes.push(`name=${fields.name}`);
|
|
232
|
+
}
|
|
233
|
+
if (changes.length > 0) {
|
|
234
|
+
log.debug(`Session ${id} updated: ${changes.join(', ')}`);
|
|
235
|
+
}
|
|
216
236
|
return true;
|
|
217
237
|
}
|
|
218
238
|
|
|
@@ -242,15 +262,17 @@ class SessionManager {
|
|
|
242
262
|
git,
|
|
243
263
|
});
|
|
244
264
|
}
|
|
265
|
+
log.debug(`Listing ${list.length} session(s)`);
|
|
245
266
|
return list;
|
|
246
267
|
}
|
|
247
268
|
|
|
248
269
|
shutdown() {
|
|
270
|
+
log.info(`Shutting down ${this.sessions.size} session(s)`);
|
|
249
271
|
for (const [_id, s] of this.sessions) {
|
|
250
272
|
try {
|
|
251
273
|
s.pty.kill();
|
|
252
|
-
} catch {
|
|
253
|
-
|
|
274
|
+
} catch (err) {
|
|
275
|
+
log.warn(`Failed to kill session ${_id}: ${err.message}`);
|
|
254
276
|
}
|
|
255
277
|
}
|
|
256
278
|
this.sessions.clear();
|
package/src/server/websocket.js
CHANGED
|
@@ -54,6 +54,7 @@ function recalcPtySize(session) {
|
|
|
54
54
|
if (minCols === session._lastCols && minRows === session._lastRows) return;
|
|
55
55
|
session._lastCols = minCols;
|
|
56
56
|
session._lastRows = minRows;
|
|
57
|
+
log.debug(`PTY resized to ${minCols}×${minRows}`);
|
|
57
58
|
session.pty.resize(minCols, minRows);
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -177,6 +178,7 @@ function setupWebSocket(wss, { auth, sessions }) {
|
|
|
177
178
|
} else if (msg.type === 'resize') {
|
|
178
179
|
const cols = Math.floor(msg.cols);
|
|
179
180
|
const rows = Math.floor(msg.rows);
|
|
181
|
+
log.debug(`Client resize request: ${cols}×${rows}`);
|
|
180
182
|
if (cols > 0 && cols <= 500 && rows > 0 && rows <= 200) {
|
|
181
183
|
ws._dims = { cols, rows };
|
|
182
184
|
ws._lastActivity = Date.now();
|
package/src/utils/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const log = require('./logger');
|
|
3
4
|
|
|
4
5
|
function git(cmd, cwd) {
|
|
5
6
|
return execSync(`git ${cmd}`, { cwd, stdio: 'pipe', timeout: 3000 }).toString().trim();
|
|
@@ -9,6 +10,7 @@ function getGitInfo(cwd) {
|
|
|
9
10
|
try {
|
|
10
11
|
git('rev-parse --is-inside-work-tree', cwd);
|
|
11
12
|
} catch {
|
|
13
|
+
log.debug(`Not a git repository: ${cwd}`);
|
|
12
14
|
return null;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -59,6 +61,7 @@ function getGitInfo(cwd) {
|
|
|
59
61
|
/* ignore */
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
log.debug(`Git info resolved for ${cwd}`);
|
|
62
65
|
return result;
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -3,6 +3,7 @@ const http = require('http');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const log = require('./logger');
|
|
6
7
|
|
|
7
8
|
const PACKAGE_NAME = 'termbeam';
|
|
8
9
|
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -114,9 +115,11 @@ function sanitizeVersion(v) {
|
|
|
114
115
|
function fetchLatestVersion(registryUrl) {
|
|
115
116
|
const url = registryUrl || REGISTRY_URL;
|
|
116
117
|
const client = url.startsWith('https') ? https : http;
|
|
118
|
+
log.debug('Fetching latest version from npm registry');
|
|
117
119
|
return new Promise((resolve) => {
|
|
118
120
|
const req = client.get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
119
121
|
if (res.statusCode !== 200) {
|
|
122
|
+
log.warn(`Registry returned HTTP ${res.statusCode}`);
|
|
120
123
|
res.resume();
|
|
121
124
|
resolve(null);
|
|
122
125
|
return;
|
|
@@ -154,8 +157,12 @@ function fetchLatestVersion(registryUrl) {
|
|
|
154
157
|
});
|
|
155
158
|
// Unref so a pending update check can't delay process exit
|
|
156
159
|
req.on('socket', (socket) => socket.unref());
|
|
157
|
-
req.on('error', () =>
|
|
160
|
+
req.on('error', (err) => {
|
|
161
|
+
log.debug(`Network error checking updates: ${err.message}`);
|
|
162
|
+
resolve(null);
|
|
163
|
+
});
|
|
158
164
|
req.on('timeout', () => {
|
|
165
|
+
log.warn('Update check timed out');
|
|
159
166
|
req.destroy();
|
|
160
167
|
resolve(null);
|
|
161
168
|
});
|
|
@@ -170,6 +177,7 @@ function fetchLatestVersion(registryUrl) {
|
|
|
170
177
|
* @returns {Promise<{current: string, latest: string|null, updateAvailable: boolean}>}
|
|
171
178
|
*/
|
|
172
179
|
async function checkForUpdate({ currentVersion, force = false } = {}) {
|
|
180
|
+
log.debug(`Update check: current=${currentVersion}`);
|
|
173
181
|
if (!currentVersion) {
|
|
174
182
|
return { current: 'unknown', latest: null, updateAvailable: false };
|
|
175
183
|
}
|
|
@@ -180,6 +188,7 @@ async function checkForUpdate({ currentVersion, force = false } = {}) {
|
|
|
180
188
|
if (cache && Date.now() - cache.checkedAt < CACHE_TTL_MS) {
|
|
181
189
|
const cachedLatest = typeof cache.latest === 'string' ? sanitizeVersion(cache.latest) : null;
|
|
182
190
|
if (cachedLatest && /^\d+\.\d+\.\d+$/.test(cachedLatest)) {
|
|
191
|
+
log.debug('Using cached update check result');
|
|
183
192
|
return {
|
|
184
193
|
current: currentVersion,
|
|
185
194
|
latest: cachedLatest,
|
|
@@ -198,10 +207,17 @@ async function checkForUpdate({ currentVersion, force = false } = {}) {
|
|
|
198
207
|
// Cache the result
|
|
199
208
|
writeCache(latest);
|
|
200
209
|
|
|
210
|
+
const updateAvailable = isNewerVersion(currentVersion, latest);
|
|
211
|
+
log.debug(
|
|
212
|
+
updateAvailable
|
|
213
|
+
? `Update available: ${currentVersion} → ${latest}`
|
|
214
|
+
: `Already on latest version: ${currentVersion}`,
|
|
215
|
+
);
|
|
216
|
+
|
|
201
217
|
return {
|
|
202
218
|
current: currentVersion,
|
|
203
219
|
latest,
|
|
204
|
-
updateAvailable
|
|
220
|
+
updateAvailable,
|
|
205
221
|
};
|
|
206
222
|
}
|
|
207
223
|
|
|
@@ -212,19 +228,23 @@ async function checkForUpdate({ currentVersion, force = false } = {}) {
|
|
|
212
228
|
function detectInstallMethod() {
|
|
213
229
|
// npx / npm exec — npm sets npm_command=exec
|
|
214
230
|
if (process.env.npm_command === 'exec') {
|
|
231
|
+
log.debug('Install method: npx');
|
|
215
232
|
return { method: 'npx', command: 'npx termbeam@latest' };
|
|
216
233
|
}
|
|
217
234
|
|
|
218
235
|
// Detect package manager from npm_execpath (set during npm/yarn/pnpm lifecycle)
|
|
219
236
|
const execPath = process.env.npm_execpath || '';
|
|
220
237
|
if (execPath.includes('yarn')) {
|
|
238
|
+
log.debug('Install method: yarn');
|
|
221
239
|
return { method: 'yarn', command: 'yarn global add termbeam@latest' };
|
|
222
240
|
}
|
|
223
241
|
if (execPath.includes('pnpm')) {
|
|
242
|
+
log.debug('Install method: pnpm');
|
|
224
243
|
return { method: 'pnpm', command: 'pnpm add -g termbeam@latest' };
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
// Default: npm global install
|
|
247
|
+
log.debug('Install method: npm');
|
|
228
248
|
return { method: 'npm', command: 'npm install -g termbeam@latest' };
|
|
229
249
|
}
|
|
230
250
|
|
package/src/utils/version.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const { execSync } = require('child_process');
|
|
3
|
+
const log = require('./logger');
|
|
3
4
|
|
|
4
5
|
function getVersion() {
|
|
5
6
|
const pkg = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
@@ -7,7 +8,12 @@ function getVersion() {
|
|
|
7
8
|
|
|
8
9
|
// If installed via npm (global or npx), use the package version as-is
|
|
9
10
|
if (process.env.npm_package_version || isInstalledGlobally()) {
|
|
10
|
-
|
|
11
|
+
log.debug(
|
|
12
|
+
`Version source: ${process.env.npm_package_version ? 'npm_package_version' : 'global install'}`,
|
|
13
|
+
);
|
|
14
|
+
const version = base;
|
|
15
|
+
log.debug(`Resolved version: ${version}`);
|
|
16
|
+
return version;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
// Running from source — git tags are the version source of truth.
|
|
@@ -27,20 +33,33 @@ function getVersion() {
|
|
|
27
33
|
const dirty = tagMatch[4];
|
|
28
34
|
|
|
29
35
|
// Exactly on a clean tag — return the tag version
|
|
30
|
-
if (!commits && !dirty)
|
|
36
|
+
if (!commits && !dirty) {
|
|
37
|
+
log.debug('Version source: git describe');
|
|
38
|
+
const version = gitVersion;
|
|
39
|
+
log.debug(`Resolved version: ${version}`);
|
|
40
|
+
return version;
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
// Build a combined semver-style dev string
|
|
33
44
|
let ver = `${gitVersion}-dev`;
|
|
34
45
|
if (commits) ver += `.${commits}`;
|
|
35
46
|
const meta = [hash ? `g${hash}` : null, dirty ? 'dirty' : null].filter(Boolean).join('.');
|
|
36
47
|
if (meta) ver += `+${meta}`;
|
|
48
|
+
log.debug('Version source: git describe');
|
|
49
|
+
log.debug(`Resolved version: ${ver}`);
|
|
37
50
|
return ver;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
// No semver tag found (e.g. bare commit hash) — fall back to package.json
|
|
41
|
-
|
|
54
|
+
log.debug('Version source: package.json fallback');
|
|
55
|
+
const version = `${base}-dev+${gitDesc}`;
|
|
56
|
+
log.debug(`Resolved version: ${version}`);
|
|
57
|
+
return version;
|
|
42
58
|
} catch {
|
|
43
|
-
|
|
59
|
+
log.debug('Version source: package.json fallback');
|
|
60
|
+
const version = `${base}-dev`;
|
|
61
|
+
log.debug(`Resolved version: ${version}`);
|
|
62
|
+
return version;
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
|