@link-assistant/hive-mind 1.41.0 → 1.43.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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/telegram-bot.mjs +47 -47
- package/src/telegram-merge-command.lib.mjs +8 -1
- package/src/telegram-start-stop-command.lib.mjs +253 -0
- package/src/version-info.lib.mjs +458 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.43.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 91479e3: Better /version command output with uniform formatting and bug fixes: add regex version parsers for all 40+ tools, fix LLD/Xvfb/Playwright MCP detection, add Playwright browser cache fallback, fail Docker build on MCP registration failure
|
|
8
|
+
|
|
9
|
+
## 1.42.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 5aa82f5: Add /stop and /start commands for telegram bot to control task acceptance per chat (Issue #1081)
|
|
14
|
+
|
|
3
15
|
## 1.41.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
package/src/telegram-bot.mjs
CHANGED
|
@@ -53,6 +53,7 @@ const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mj
|
|
|
53
53
|
const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
|
|
54
54
|
const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
|
|
55
55
|
const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
|
|
56
|
+
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
56
57
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
57
58
|
// Import bot launcher with exponential backoff retry (issue #1240)
|
|
58
59
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
@@ -603,23 +604,17 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
|
|
|
603
604
|
}
|
|
604
605
|
|
|
605
606
|
bot.command('help', async ctx => {
|
|
606
|
-
|
|
607
|
-
console.log('[VERBOSE] /help command received');
|
|
608
|
-
}
|
|
607
|
+
VERBOSE && console.log('[VERBOSE] /help command received');
|
|
609
608
|
|
|
610
609
|
// Ignore messages sent before bot started
|
|
611
610
|
if (isOldMessage(ctx)) {
|
|
612
|
-
|
|
613
|
-
console.log('[VERBOSE] /help ignored: old message');
|
|
614
|
-
}
|
|
611
|
+
VERBOSE && console.log('[VERBOSE] /help ignored: old message');
|
|
615
612
|
return;
|
|
616
613
|
}
|
|
617
614
|
|
|
618
615
|
// Ignore forwarded or reply messages
|
|
619
616
|
if (isForwardedOrReply(ctx)) {
|
|
620
|
-
|
|
621
|
-
console.log('[VERBOSE] /help ignored: forwarded or reply');
|
|
622
|
-
}
|
|
617
|
+
VERBOSE && console.log('[VERBOSE] /help ignored: forwarded or reply');
|
|
623
618
|
return;
|
|
624
619
|
}
|
|
625
620
|
|
|
@@ -628,6 +623,19 @@ bot.command('help', async ctx => {
|
|
|
628
623
|
const chatTitle = ctx.chat.title || 'Private Chat';
|
|
629
624
|
|
|
630
625
|
let message = '🤖 *SwarmMindBot Help*\n\n';
|
|
626
|
+
|
|
627
|
+
// Show stopped status if chat is stopped (issue #1081)
|
|
628
|
+
if (isChatStopped(chatId)) {
|
|
629
|
+
const stopInfo = getChatStopInfo(chatId);
|
|
630
|
+
const reason = stopInfo?.reason || DEFAULT_STOP_REASON;
|
|
631
|
+
message += '🛑 *Bot Status: STOPPED*\n';
|
|
632
|
+
message += `Reason: ${reason}\n`;
|
|
633
|
+
if (stopInfo?.stoppedAt) {
|
|
634
|
+
message += `Stopped: ${stopInfo.stoppedAt.toISOString()}\n`;
|
|
635
|
+
}
|
|
636
|
+
message += 'Use /start (chat owner only) to resume.\n\n';
|
|
637
|
+
}
|
|
638
|
+
|
|
631
639
|
message += '📋 *Diagnostic Information:*\n';
|
|
632
640
|
message += `• Chat ID: \`${chatId}\`\n`;
|
|
633
641
|
message += `• Chat Type: ${chatType}\n`;
|
|
@@ -666,8 +674,10 @@ bot.command('help', async ctx => {
|
|
|
666
674
|
message += '*/merge* - Merge queue (experimental)\n';
|
|
667
675
|
message += 'Usage: `/merge <github-repo-url>`\n';
|
|
668
676
|
message += "Merges all PRs with 'ready' label sequentially.\n";
|
|
669
|
-
message += '*/help* - Show this help message\n
|
|
670
|
-
message += '
|
|
677
|
+
message += '*/help* - Show this help message\n';
|
|
678
|
+
message += '*/stop* - Stop accepting new tasks (owner only)\n';
|
|
679
|
+
message += '*/start* - Resume accepting tasks (owner only)\n\n';
|
|
680
|
+
message += '⚠️ *Note:* /solve, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats.\n\n';
|
|
671
681
|
message += '🔧 *Common Options:*\n';
|
|
672
682
|
message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
|
|
673
683
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
@@ -693,36 +703,20 @@ bot.command('help', async ctx => {
|
|
|
693
703
|
});
|
|
694
704
|
|
|
695
705
|
bot.command('limits', async ctx => {
|
|
696
|
-
|
|
697
|
-
console.log('[VERBOSE] /limits command received');
|
|
698
|
-
}
|
|
706
|
+
VERBOSE && console.log('[VERBOSE] /limits command received');
|
|
699
707
|
|
|
700
708
|
// Add breadcrumb for error tracking
|
|
701
|
-
await addBreadcrumb({
|
|
702
|
-
category: 'telegram.command',
|
|
703
|
-
message: '/limits command received',
|
|
704
|
-
level: 'info',
|
|
705
|
-
data: {
|
|
706
|
-
chatId: ctx.chat?.id,
|
|
707
|
-
chatType: ctx.chat?.type,
|
|
708
|
-
userId: ctx.from?.id,
|
|
709
|
-
username: ctx.from?.username,
|
|
710
|
-
},
|
|
711
|
-
});
|
|
709
|
+
await addBreadcrumb({ category: 'telegram.command', message: '/limits command received', level: 'info', data: { chatId: ctx.chat?.id, chatType: ctx.chat?.type, userId: ctx.from?.id, username: ctx.from?.username } });
|
|
712
710
|
|
|
713
711
|
// Ignore messages sent before bot started
|
|
714
712
|
if (isOldMessage(ctx)) {
|
|
715
|
-
|
|
716
|
-
console.log('[VERBOSE] /limits ignored: old message');
|
|
717
|
-
}
|
|
713
|
+
VERBOSE && console.log('[VERBOSE] /limits ignored: old message');
|
|
718
714
|
return;
|
|
719
715
|
}
|
|
720
716
|
|
|
721
717
|
// Ignore forwarded or reply messages
|
|
722
718
|
if (isForwardedOrReply(ctx)) {
|
|
723
|
-
|
|
724
|
-
console.log('[VERBOSE] /limits ignored: forwarded or reply');
|
|
725
|
-
}
|
|
719
|
+
VERBOSE && console.log('[VERBOSE] /limits ignored: forwarded or reply');
|
|
726
720
|
return;
|
|
727
721
|
}
|
|
728
722
|
|
|
@@ -736,9 +730,7 @@ bot.command('limits', async ctx => {
|
|
|
736
730
|
|
|
737
731
|
const chatId = ctx.chat.id;
|
|
738
732
|
if (!isChatAuthorized(chatId)) {
|
|
739
|
-
|
|
740
|
-
console.log('[VERBOSE] /limits ignored: chat not authorized');
|
|
741
|
-
}
|
|
733
|
+
VERBOSE && console.log('[VERBOSE] /limits ignored: chat not authorized');
|
|
742
734
|
await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot. Please contact the bot administrator.`, { reply_to_message_id: ctx.message.message_id });
|
|
743
735
|
return;
|
|
744
736
|
}
|
|
@@ -807,6 +799,8 @@ registerMergeCommand(bot, {
|
|
|
807
799
|
isGroupChat: _isGroupChat,
|
|
808
800
|
isChatAuthorized,
|
|
809
801
|
addBreadcrumb,
|
|
802
|
+
isChatStopped,
|
|
803
|
+
getStoppedChatRejectMessage,
|
|
810
804
|
});
|
|
811
805
|
|
|
812
806
|
// Register /solve_queue command from separate module (issue #1232)
|
|
@@ -886,10 +880,15 @@ async function handleSolveCommand(ctx) {
|
|
|
886
880
|
return;
|
|
887
881
|
}
|
|
888
882
|
|
|
889
|
-
if (
|
|
890
|
-
|
|
883
|
+
// Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
|
|
884
|
+
if (isChatStopped(chatId)) {
|
|
885
|
+
VERBOSE && console.log('[VERBOSE] /solve rejected: chat is stopped');
|
|
886
|
+
await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Solve'), { reply_to_message_id: ctx.message.message_id });
|
|
887
|
+
return;
|
|
891
888
|
}
|
|
892
889
|
|
|
890
|
+
VERBOSE && console.log('[VERBOSE] /solve passed all checks, executing...');
|
|
891
|
+
|
|
893
892
|
let userArgs = parseCommandArgs(ctx.message.text);
|
|
894
893
|
|
|
895
894
|
// Check if this is a reply to a message and user didn't provide URL as first argument
|
|
@@ -1109,10 +1108,15 @@ async function handleHiveCommand(ctx) {
|
|
|
1109
1108
|
return;
|
|
1110
1109
|
}
|
|
1111
1110
|
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1111
|
+
// Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
|
|
1112
|
+
if (isChatStopped(chatId)) {
|
|
1113
|
+
VERBOSE && console.log('[VERBOSE] /hive rejected: chat is stopped');
|
|
1114
|
+
await safeReply(ctx, getStoppedChatRejectMessage(chatId, 'Hive'), { reply_to_message_id: ctx.message.message_id });
|
|
1115
|
+
return;
|
|
1114
1116
|
}
|
|
1115
1117
|
|
|
1118
|
+
VERBOSE && console.log('[VERBOSE] /hive passed all checks, executing...');
|
|
1119
|
+
|
|
1116
1120
|
const userArgs = parseCommandArgs(ctx.message.text);
|
|
1117
1121
|
|
|
1118
1122
|
// Issue #1102: Allow issues_list/pulls_list URLs and normalize to repo URLs
|
|
@@ -1197,16 +1201,12 @@ async function handleHiveCommand(ctx) {
|
|
|
1197
1201
|
|
|
1198
1202
|
bot.command(/^hive$/i, handleHiveCommand);
|
|
1199
1203
|
|
|
1200
|
-
// Register
|
|
1201
|
-
// This keeps telegram-bot.mjs under the 1500 line limit
|
|
1204
|
+
// Register commands from separate modules (keeps telegram-bot.mjs under line limit)
|
|
1202
1205
|
const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
isGroupChat: _isGroupChat,
|
|
1208
|
-
isChatAuthorized,
|
|
1209
|
-
});
|
|
1206
|
+
const { registerStartStopCommands } = await import('./telegram-start-stop-command.lib.mjs');
|
|
1207
|
+
const commandOptions = { VERBOSE, isOldMessage, isForwardedOrReply, isGroupChat: _isGroupChat, isChatAuthorized };
|
|
1208
|
+
registerTopCommand(bot, commandOptions);
|
|
1209
|
+
registerStartStopCommands(bot, commandOptions); // issue #1081
|
|
1210
1210
|
|
|
1211
1211
|
// Add message listener for verbose debugging
|
|
1212
1212
|
if (VERBOSE) {
|
|
@@ -133,7 +133,7 @@ function formatUserError(error, verbose) {
|
|
|
133
133
|
* @param {Function} options.addBreadcrumb - Function to add breadcrumbs for monitoring
|
|
134
134
|
*/
|
|
135
135
|
export function registerMergeCommand(bot, options) {
|
|
136
|
-
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb } = options;
|
|
136
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized, addBreadcrumb, isChatStopped, getStoppedChatRejectMessage } = options;
|
|
137
137
|
|
|
138
138
|
bot.command(/^merge$/i, async ctx => {
|
|
139
139
|
VERBOSE && console.log('[VERBOSE] /merge command received');
|
|
@@ -161,6 +161,13 @@ export function registerMergeCommand(bot, options) {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Check if chat is stopped (issue #1081) - reject with same style as queue rejected mode
|
|
165
|
+
if (isChatStopped && isChatStopped(chatId)) {
|
|
166
|
+
VERBOSE && console.log('[VERBOSE] /merge rejected: chat is stopped');
|
|
167
|
+
const rejectMsg = getStoppedChatRejectMessage ? getStoppedChatRejectMessage(chatId, 'Merge') : '❌ Merge command rejected.';
|
|
168
|
+
return await ctx.reply(rejectMsg, { reply_to_message_id: ctx.message.message_id });
|
|
169
|
+
}
|
|
170
|
+
|
|
164
171
|
// Parse arguments
|
|
165
172
|
const args = parseCommandArgs(ctx.message.text);
|
|
166
173
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram /start and /stop command implementation
|
|
3
|
+
*
|
|
4
|
+
* This module provides the /start and /stop command functionality for the Telegram bot,
|
|
5
|
+
* allowing chat owners to control whether the bot accepts new tasks in a specific chat.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Per-chat stop state management
|
|
9
|
+
* - Owner-only access control (creator only, not admins)
|
|
10
|
+
* - Graceful stop: existing queue items continue to process
|
|
11
|
+
* - Read-only commands (/help, /limits, /version) remain available when stopped
|
|
12
|
+
* - Write commands (/solve, /hive) are rejected when stopped
|
|
13
|
+
*
|
|
14
|
+
* @see https://github.com/link-assistant/hive-mind/issues/1081
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Store stopped chats: Map<chatId, { stoppedAt: Date, stoppedBy: { id, username, firstName }, reason?: string }>
|
|
18
|
+
const stoppedChats = new Map();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a chat is currently stopped
|
|
22
|
+
* @param {number} chatId - The chat ID to check
|
|
23
|
+
* @returns {boolean} True if the chat is stopped
|
|
24
|
+
*/
|
|
25
|
+
export function isChatStopped(chatId) {
|
|
26
|
+
return stoppedChats.has(chatId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get stop information for a chat
|
|
31
|
+
* @param {number} chatId - The chat ID
|
|
32
|
+
* @returns {Object|null} Stop info or null if not stopped
|
|
33
|
+
*/
|
|
34
|
+
export function getChatStopInfo(chatId) {
|
|
35
|
+
return stoppedChats.get(chatId) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set chat stopped state
|
|
40
|
+
* @param {number} chatId - The chat ID
|
|
41
|
+
* @param {boolean} stopped - Whether to stop or start the chat
|
|
42
|
+
* @param {Object} user - The user who issued the command (for stop)
|
|
43
|
+
* @param {string} [reason] - Optional reason for stopping (only used when stopped=true)
|
|
44
|
+
*/
|
|
45
|
+
export function setChatStopped(chatId, stopped, user = null, reason = null) {
|
|
46
|
+
if (stopped) {
|
|
47
|
+
stoppedChats.set(chatId, {
|
|
48
|
+
stoppedAt: new Date(),
|
|
49
|
+
stoppedBy: user
|
|
50
|
+
? {
|
|
51
|
+
id: user.id,
|
|
52
|
+
username: user.username,
|
|
53
|
+
firstName: user.first_name,
|
|
54
|
+
}
|
|
55
|
+
: null,
|
|
56
|
+
reason: reason || null,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
stoppedChats.delete(chatId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get all stopped chats (for debugging/admin purposes)
|
|
65
|
+
* @returns {Map} Map of stopped chats
|
|
66
|
+
*/
|
|
67
|
+
export function getStoppedChats() {
|
|
68
|
+
return stoppedChats;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default reason used when no custom reason is provided for /stop
|
|
73
|
+
*/
|
|
74
|
+
export const DEFAULT_STOP_REASON = 'This bot is currently not accepting new tasks.';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get rejection message for when a command is used on a stopped chat.
|
|
78
|
+
* Matches the style of queue `rejected` mode output for consistency.
|
|
79
|
+
* @param {number} chatId - The chat ID
|
|
80
|
+
* @param {string} commandName - The command that was rejected (e.g., 'Solve', 'Hive')
|
|
81
|
+
* @returns {string} Markdown-formatted rejection message
|
|
82
|
+
*/
|
|
83
|
+
export function getStoppedChatRejectMessage(chatId, commandName = 'Command') {
|
|
84
|
+
const stopInfo = getChatStopInfo(chatId);
|
|
85
|
+
const reason = stopInfo?.reason || DEFAULT_STOP_REASON;
|
|
86
|
+
return `❌ ${commandName} command rejected.\n\n🚫 Reason: ${reason}\n\nUse /start to resume (chat owner only).`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registers the /start and /stop command handlers with the bot
|
|
91
|
+
* @param {Object} bot - The Telegraf bot instance
|
|
92
|
+
* @param {Object} options - Options object
|
|
93
|
+
* @param {boolean} options.VERBOSE - Whether to enable verbose logging
|
|
94
|
+
* @param {Function} options.isOldMessage - Function to check if message is old
|
|
95
|
+
* @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
|
|
96
|
+
* @param {Function} options.isGroupChat - Function to check if chat is a group
|
|
97
|
+
* @param {Function} options.isChatAuthorized - Function to check if chat is authorized
|
|
98
|
+
*/
|
|
99
|
+
export function registerStartStopCommands(bot, options) {
|
|
100
|
+
const { VERBOSE = false, isOldMessage, isForwardedOrReply, isGroupChat, isChatAuthorized } = options;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate command context: checks old message, forwarded, group chat, authorized, and owner status.
|
|
104
|
+
* @param {Object} ctx - Telegraf context
|
|
105
|
+
* @param {string} cmdName - Command name for logging (e.g., '/stop', '/start')
|
|
106
|
+
* @param {Object} [opts] - Options
|
|
107
|
+
* @param {boolean} [opts.allowPrivate] - If true, skip group chat check (for /start welcome)
|
|
108
|
+
* @returns {Promise<{valid: boolean, chatId?: number, isPrivate?: boolean}>}
|
|
109
|
+
*/
|
|
110
|
+
async function validateOwnerCommand(ctx, cmdName, opts = {}) {
|
|
111
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} command received`);
|
|
112
|
+
if (isOldMessage(ctx)) {
|
|
113
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: old message`);
|
|
114
|
+
return { valid: false };
|
|
115
|
+
}
|
|
116
|
+
if (isForwardedOrReply(ctx)) {
|
|
117
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: forwarded or reply`);
|
|
118
|
+
return { valid: false };
|
|
119
|
+
}
|
|
120
|
+
if (!isGroupChat(ctx)) {
|
|
121
|
+
if (opts.allowPrivate) return { valid: false, isPrivate: true };
|
|
122
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: not a group chat`);
|
|
123
|
+
await ctx.reply(`❌ The ${cmdName} command only works in group chats.`, { reply_to_message_id: ctx.message.message_id });
|
|
124
|
+
return { valid: false };
|
|
125
|
+
}
|
|
126
|
+
const chatId = ctx.chat.id;
|
|
127
|
+
if (!isChatAuthorized(chatId)) {
|
|
128
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: chat not authorized`);
|
|
129
|
+
await ctx.reply(`❌ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
|
|
130
|
+
return { valid: false };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
|
|
134
|
+
if (chatMember.status !== 'creator') {
|
|
135
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} ignored: user is not chat owner`);
|
|
136
|
+
await ctx.reply('❌ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
|
|
137
|
+
return { valid: false };
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('[ERROR] Failed to check chat member status:', error);
|
|
141
|
+
await ctx.reply('❌ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
|
|
142
|
+
return { valid: false };
|
|
143
|
+
}
|
|
144
|
+
VERBOSE && console.log(`[VERBOSE] ${cmdName} passed all checks`);
|
|
145
|
+
return { valid: true, chatId };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// /stop command - stop accepting new tasks in this chat
|
|
149
|
+
// Only accessible by chat owner (creator)
|
|
150
|
+
bot.command('stop', async ctx => {
|
|
151
|
+
const check = await validateOwnerCommand(ctx, '/stop');
|
|
152
|
+
if (!check.valid) return;
|
|
153
|
+
const chatId = check.chatId;
|
|
154
|
+
|
|
155
|
+
// Check if already stopped
|
|
156
|
+
if (isChatStopped(chatId)) {
|
|
157
|
+
const stopInfo = getChatStopInfo(chatId);
|
|
158
|
+
const stoppedAtStr = stopInfo?.stoppedAt ? stopInfo.stoppedAt.toISOString() : 'unknown';
|
|
159
|
+
let alreadyStoppedMsg = `ℹ️ Bot is already stopped in this chat.\n\nStopped at: ${stoppedAtStr}`;
|
|
160
|
+
if (stopInfo?.reason) {
|
|
161
|
+
alreadyStoppedMsg += `\nReason: ${stopInfo.reason}`;
|
|
162
|
+
}
|
|
163
|
+
alreadyStoppedMsg += '\n\nUse /start to resume accepting tasks.';
|
|
164
|
+
await ctx.reply(alreadyStoppedMsg, {
|
|
165
|
+
reply_to_message_id: ctx.message.message_id,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Parse optional reason from message text (anything after "/stop ")
|
|
171
|
+
// Supports: /stop reason, /stop "reason", /stop 'reason'
|
|
172
|
+
const messageText = ctx.message.text || '';
|
|
173
|
+
let reason = messageText.replace(/^\/stop(@\w+)?\s*/i, '').trim() || null;
|
|
174
|
+
// Strip surrounding quotes (single or double) from reason
|
|
175
|
+
if (reason && ((reason.startsWith('"') && reason.endsWith('"')) || (reason.startsWith("'") && reason.endsWith("'")))) {
|
|
176
|
+
reason = reason.slice(1, -1).trim() || null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (VERBOSE && reason) {
|
|
180
|
+
console.log(`[VERBOSE] /stop reason: ${reason}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set chat as stopped with optional reason
|
|
184
|
+
setChatStopped(chatId, true, ctx.from, reason);
|
|
185
|
+
|
|
186
|
+
if (VERBOSE) {
|
|
187
|
+
console.log(`[VERBOSE] Chat ${chatId} is now stopped`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let stopMessage = '🛑 *Bot Stopped*\n\n' + 'This bot is now in read-only mode for this chat.\n\n';
|
|
191
|
+
if (reason) {
|
|
192
|
+
stopMessage += `*Reason:* ${reason}\n\n`;
|
|
193
|
+
}
|
|
194
|
+
stopMessage += '*Disabled commands:*\n' + '• /solve - No new issues will be accepted\n' + '• /hive - No new hive commands will be accepted\n' + '• /merge - No new merge operations will be accepted\n\n' + '*Still available:*\n' + '• /help - Show help\n' + '• /limits - Show usage limits\n' + '• /version - Show version info\n' + '• /start - Resume accepting tasks (owner only)\n\n' + '💡 Any tasks already in queue will continue to process.';
|
|
195
|
+
|
|
196
|
+
await ctx.reply(stopMessage, {
|
|
197
|
+
parse_mode: 'Markdown',
|
|
198
|
+
reply_to_message_id: ctx.message.message_id,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// /start command - resume accepting new tasks in this chat
|
|
203
|
+
// Only accessible by chat owner (creator)
|
|
204
|
+
// Note: This overrides Telegram's default /start behavior, but that's intentional
|
|
205
|
+
// as in group chats we want this to control the bot's task acceptance
|
|
206
|
+
bot.command('start', async ctx => {
|
|
207
|
+
const check = await validateOwnerCommand(ctx, '/start', { allowPrivate: true });
|
|
208
|
+
if (!check.valid) {
|
|
209
|
+
// In private chats, show a welcome message instead
|
|
210
|
+
if (check.isPrivate) {
|
|
211
|
+
VERBOSE && console.log('[VERBOSE] /start in private chat: showing welcome');
|
|
212
|
+
await ctx.reply('👋 *Welcome to SwarmMindBot!*\n\n' + 'This bot helps solve GitHub issues using AI.\n\n' + 'To use this bot:\n' + '1. Add me to a group chat\n' + '2. Make me an admin\n' + '3. Use /solve to solve GitHub issues\n\n' + 'Use /help in a group chat for more information.', { parse_mode: 'Markdown' });
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const chatId = check.chatId;
|
|
217
|
+
|
|
218
|
+
// Check if already running (not stopped)
|
|
219
|
+
if (!isChatStopped(chatId)) {
|
|
220
|
+
await ctx.reply('ℹ️ Bot is already accepting tasks in this chat.\n\nUse /help to see available commands.', {
|
|
221
|
+
reply_to_message_id: ctx.message.message_id,
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get stop info for the message
|
|
227
|
+
const stopInfo = getChatStopInfo(chatId);
|
|
228
|
+
const stoppedDuration = stopInfo?.stoppedAt ? Math.round((Date.now() - stopInfo.stoppedAt.getTime()) / 1000) : 0;
|
|
229
|
+
|
|
230
|
+
// Clear the stopped state
|
|
231
|
+
setChatStopped(chatId, false);
|
|
232
|
+
|
|
233
|
+
if (VERBOSE) {
|
|
234
|
+
console.log(`[VERBOSE] Chat ${chatId} is now started`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let durationStr = '';
|
|
238
|
+
if (stoppedDuration > 0) {
|
|
239
|
+
if (stoppedDuration < 60) {
|
|
240
|
+
durationStr = `${stoppedDuration} seconds`;
|
|
241
|
+
} else if (stoppedDuration < 3600) {
|
|
242
|
+
durationStr = `${Math.round(stoppedDuration / 60)} minutes`;
|
|
243
|
+
} else {
|
|
244
|
+
durationStr = `${Math.round(stoppedDuration / 3600)} hours`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await ctx.reply('✅ *Bot Started*\n\n' + 'This bot is now accepting tasks in this chat.\n\n' + (durationStr ? `Bot was stopped for ${durationStr}.\n\n` : '') + 'Use /help to see available commands.', {
|
|
249
|
+
parse_mode: 'Markdown',
|
|
250
|
+
reply_to_message_id: ctx.message.message_id,
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
package/src/version-info.lib.mjs
CHANGED
|
@@ -33,6 +33,397 @@ async function execCommandAsync(command, timeout = 5000) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Per-tool regex parsers to normalize raw --version output into uniform format:
|
|
38
|
+
* <version> (<commit>, <revision>, <date>, etc.)
|
|
39
|
+
*
|
|
40
|
+
* Each parser returns { version, extra[] } or null if it doesn't match.
|
|
41
|
+
* The `version` is the most specific version string (for bug reporting).
|
|
42
|
+
* Items in `extra` are joined with ", " and placed in parentheses.
|
|
43
|
+
*
|
|
44
|
+
* @type {Record<string, (raw: string) => {version: string, extra: string[]} | null>}
|
|
45
|
+
*/
|
|
46
|
+
const VERSION_PARSERS = {
|
|
47
|
+
// rustc 1.94.1 (e408947bf 2026-03-25)
|
|
48
|
+
rust: raw => {
|
|
49
|
+
const m = raw.match(/^rustc\s+([\d.]+(?:-\S+)?)\s*(?:\(([^)]+)\))?/);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const extra = m[2] ? m[2].trim().split(/\s+/) : [];
|
|
52
|
+
return { version: m[1], extra };
|
|
53
|
+
},
|
|
54
|
+
// cargo 1.94.1 (29ea6fb6a 2026-03-24)
|
|
55
|
+
cargo: raw => {
|
|
56
|
+
const m = raw.match(/^cargo\s+([\d.]+(?:-\S+)?)\s*(?:\(([^)]+)\))?/);
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
const extra = m[2] ? m[2].trim().split(/\s+/) : [];
|
|
59
|
+
return { version: m[1], extra };
|
|
60
|
+
},
|
|
61
|
+
// go version go1.26.1 linux/amd64
|
|
62
|
+
go: raw => {
|
|
63
|
+
const m = raw.match(/go([\d.]+(?:\S*)?)\s+(.*)/);
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
return { version: m[1], extra: [m[2].trim()] };
|
|
66
|
+
},
|
|
67
|
+
// PHP 8.3.30 (cli) (built: Jan 13 2026 22:36:55) (NTS)
|
|
68
|
+
php: raw => {
|
|
69
|
+
const m = raw.match(/^PHP\s+([\d.]+(?:-\S+)?)\s*(.*)/);
|
|
70
|
+
if (!m) return null;
|
|
71
|
+
const tags = [];
|
|
72
|
+
const parts = m[2].matchAll(/\(([^)]+)\)/g);
|
|
73
|
+
for (const p of parts) tags.push(p[1]);
|
|
74
|
+
return { version: m[1], extra: tags };
|
|
75
|
+
},
|
|
76
|
+
// openjdk version "21" 2023-09-19 LTS
|
|
77
|
+
java: raw => {
|
|
78
|
+
const m = raw.match(/version\s+"([^"]+)"(?:\s+(.+))?/);
|
|
79
|
+
if (!m) return null;
|
|
80
|
+
return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
|
|
81
|
+
},
|
|
82
|
+
// gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 — use full distro version
|
|
83
|
+
gcc: raw => {
|
|
84
|
+
const m = raw.match(/^gcc\s+(?:\((\S+)\s+([\d.]+\S*)\)\s+)?([\d.]+)/);
|
|
85
|
+
if (!m) return null;
|
|
86
|
+
// If distro info present, use full distro version (e.g. 13.3.0-6ubuntu2~24.04.1)
|
|
87
|
+
if (m[1] && m[2]) return { version: m[2], extra: [] };
|
|
88
|
+
return { version: m[3], extra: [] };
|
|
89
|
+
},
|
|
90
|
+
// g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 — use full distro version
|
|
91
|
+
gpp: raw => {
|
|
92
|
+
const m = raw.match(/^g\+\+\s+(?:\((\S+)\s+([\d.]+\S*)\)\s+)?([\d.]+)/);
|
|
93
|
+
if (!m) return null;
|
|
94
|
+
// If distro info present, use full distro version (e.g. 13.3.0-6ubuntu2~24.04.1)
|
|
95
|
+
if (m[1] && m[2]) return { version: m[2], extra: [] };
|
|
96
|
+
return { version: m[3], extra: [] };
|
|
97
|
+
},
|
|
98
|
+
// clang version 17.0.0 (https://github.com/... commit)
|
|
99
|
+
clang: raw => {
|
|
100
|
+
const m = raw.match(/^clang\s+version\s+([\d.]+(?:-\S+)?)\s*(?:\(([^)]+)\))?/);
|
|
101
|
+
if (!m) return null;
|
|
102
|
+
return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
|
|
103
|
+
},
|
|
104
|
+
// LLD 17.0.0 (compatible with GNU linkers) — only version number matters
|
|
105
|
+
lld: raw => {
|
|
106
|
+
const m = raw.match(/^LLD\s+([\d.]+)/);
|
|
107
|
+
if (!m) return null;
|
|
108
|
+
return { version: m[1], extra: [] };
|
|
109
|
+
},
|
|
110
|
+
// Python 3.14.3
|
|
111
|
+
python: raw => {
|
|
112
|
+
const m = raw.match(/^Python\s+([\d.]+(?:\S*)?)/);
|
|
113
|
+
if (!m) return null;
|
|
114
|
+
return { version: m[1], extra: [] };
|
|
115
|
+
},
|
|
116
|
+
// ruby 3.4.9 (2026-03-11 revision 76cca827ab) +PRISM [x86_64-linux]
|
|
117
|
+
ruby: raw => {
|
|
118
|
+
const m = raw.match(/^ruby\s+([\d.]+(?:p\d+)?)\s*(?:\(([^)]+)\))?\s*(.*)/);
|
|
119
|
+
if (!m) return null;
|
|
120
|
+
const extra = [];
|
|
121
|
+
if (m[2]) extra.push(m[2].trim());
|
|
122
|
+
const tail = m[3] ? m[3].trim() : '';
|
|
123
|
+
if (tail) extra.push(tail);
|
|
124
|
+
return { version: m[1], extra };
|
|
125
|
+
},
|
|
126
|
+
// Kotlin version 2.3.20-release-208 (JRE 21+35-LTS)
|
|
127
|
+
kotlin: raw => {
|
|
128
|
+
const m = raw.match(/^Kotlin\s+version\s+([\d.\-\w]+)\s*(?:\(([^)]+)\))?/);
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
|
|
131
|
+
},
|
|
132
|
+
// Swift version 6.0.3 (swift-6.0.3-RELEASE)
|
|
133
|
+
swift: raw => {
|
|
134
|
+
const m = raw.match(/^Swift\s+version\s+([\d.]+(?:\.\d+)?)\s*(?:\(([^)]+)\))?/);
|
|
135
|
+
if (!m) return null;
|
|
136
|
+
return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
|
|
137
|
+
},
|
|
138
|
+
// R version 4.3.3 (2024-02-29) -- "Angel Food Cake"
|
|
139
|
+
r: raw => {
|
|
140
|
+
const m = raw.match(/^R\s+version\s+([\d.]+)\s*(?:\(([^)]+)\))?(?:\s+--\s+"([^"]+)")?/);
|
|
141
|
+
if (!m) return null;
|
|
142
|
+
const extra = [];
|
|
143
|
+
if (m[2]) extra.push(m[2]);
|
|
144
|
+
if (m[3]) extra.push(m[3]);
|
|
145
|
+
return { version: m[1], extra };
|
|
146
|
+
},
|
|
147
|
+
// git version 2.43.0
|
|
148
|
+
git: raw => {
|
|
149
|
+
const m = raw.match(/^git\s+version\s+([\d.]+)/);
|
|
150
|
+
if (!m) return null;
|
|
151
|
+
return { version: m[1], extra: [] };
|
|
152
|
+
},
|
|
153
|
+
// gh version 2.89.0 (2026-03-26)
|
|
154
|
+
gh: raw => {
|
|
155
|
+
const m = raw.match(/^gh\s+version\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
156
|
+
if (!m) return null;
|
|
157
|
+
return { version: m[1], extra: m[2] ? [m[2]] : [] };
|
|
158
|
+
},
|
|
159
|
+
// glab version 1.36.0
|
|
160
|
+
glab: raw => {
|
|
161
|
+
const m = raw.match(/^glab\s+version\s+([\d.]+)/);
|
|
162
|
+
if (!m) return null;
|
|
163
|
+
return { version: m[1], extra: [] };
|
|
164
|
+
},
|
|
165
|
+
// curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 ...
|
|
166
|
+
curl: raw => {
|
|
167
|
+
const m = raw.match(/^curl\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
168
|
+
if (!m) return null;
|
|
169
|
+
return { version: m[1], extra: m[2] ? [m[2]] : [] };
|
|
170
|
+
},
|
|
171
|
+
// GNU Wget 1.21.4 built on linux-gnu.
|
|
172
|
+
wget: raw => {
|
|
173
|
+
const m = raw.match(/^GNU\s+Wget\s+([\d.]+)/);
|
|
174
|
+
if (!m) return null;
|
|
175
|
+
return { version: m[1], extra: [] };
|
|
176
|
+
},
|
|
177
|
+
// cmake version 3.28.3
|
|
178
|
+
cmake: raw => {
|
|
179
|
+
const m = raw.match(/^cmake\s+version\s+([\d.]+)/);
|
|
180
|
+
if (!m) return null;
|
|
181
|
+
return { version: m[1], extra: [] };
|
|
182
|
+
},
|
|
183
|
+
// GNU Make 4.3
|
|
184
|
+
make: raw => {
|
|
185
|
+
const m = raw.match(/^GNU\s+Make\s+([\d.]+)/);
|
|
186
|
+
if (!m) return null;
|
|
187
|
+
return { version: m[1], extra: [] };
|
|
188
|
+
},
|
|
189
|
+
// NASM version 2.16.01
|
|
190
|
+
nasm: raw => {
|
|
191
|
+
const m = raw.match(/^NASM\s+version\s+([\d.]+)/);
|
|
192
|
+
if (!m) return null;
|
|
193
|
+
return { version: m[1], extra: [] };
|
|
194
|
+
},
|
|
195
|
+
// flat assembler version 1.73.32
|
|
196
|
+
fasm: raw => {
|
|
197
|
+
const m = raw.match(/version\s+([\d.]+)/);
|
|
198
|
+
if (!m) return null;
|
|
199
|
+
return { version: m[1], extra: [] };
|
|
200
|
+
},
|
|
201
|
+
// Screen version 4.09.01 (GNU) 20-Aug-23
|
|
202
|
+
screen: raw => {
|
|
203
|
+
const m = raw.match(/^Screen\s+version\s+([\d.]+)\s*(?:\(([^)]+)\))?\s*(.*)/);
|
|
204
|
+
if (!m) return null;
|
|
205
|
+
const extra = [];
|
|
206
|
+
if (m[2]) extra.push(m[2]);
|
|
207
|
+
if (m[3] && m[3].trim()) extra.push(m[3].trim());
|
|
208
|
+
return { version: m[1], extra };
|
|
209
|
+
},
|
|
210
|
+
// expect version 5.45.4
|
|
211
|
+
expect: raw => {
|
|
212
|
+
const m = raw.match(/^expect\s+version\s+([\d.]+)/);
|
|
213
|
+
if (!m) return null;
|
|
214
|
+
return { version: m[1], extra: [] };
|
|
215
|
+
},
|
|
216
|
+
// The OCaml toplevel, version 5.4.1
|
|
217
|
+
ocaml: raw => {
|
|
218
|
+
const m = raw.match(/version\s+([\d.]+)/);
|
|
219
|
+
if (!m) return null;
|
|
220
|
+
return { version: m[1], extra: [] };
|
|
221
|
+
},
|
|
222
|
+
// The Rocq Prover, version 9.1.1
|
|
223
|
+
rocq: raw => {
|
|
224
|
+
const m = raw.match(/version\s+([\d.]+)/);
|
|
225
|
+
if (!m) return null;
|
|
226
|
+
return { version: m[1], extra: [] };
|
|
227
|
+
},
|
|
228
|
+
// elan 4.2.1 (3d5138e15 2026-03-18)
|
|
229
|
+
elan: raw => {
|
|
230
|
+
const m = raw.match(/^elan\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
231
|
+
if (!m) return null;
|
|
232
|
+
const extra = m[2] ? m[2].trim().split(/\s+/) : [];
|
|
233
|
+
return { version: m[1], extra };
|
|
234
|
+
},
|
|
235
|
+
// Lean (version 4.29.0, x86_64-unknown-linux-gnu, commit abc123, Release)
|
|
236
|
+
lean: raw => {
|
|
237
|
+
const m = raw.match(/version\s+([\d.]+)(?:,\s*(.+?))\)?$/);
|
|
238
|
+
if (!m) return null;
|
|
239
|
+
const extra = m[2]
|
|
240
|
+
? m[2]
|
|
241
|
+
.split(',')
|
|
242
|
+
.map(s => s.trim().replace(/\)$/, ''))
|
|
243
|
+
.filter(Boolean)
|
|
244
|
+
: [];
|
|
245
|
+
return { version: m[1], extra };
|
|
246
|
+
},
|
|
247
|
+
// Google Chrome 146.0.7680.164
|
|
248
|
+
chrome: raw => {
|
|
249
|
+
const m = raw.match(/^Google\s+Chrome\s+([\d.]+)/);
|
|
250
|
+
if (!m) return null;
|
|
251
|
+
return { version: m[1], extra: [] };
|
|
252
|
+
},
|
|
253
|
+
// Chromium 137.0.7151.0
|
|
254
|
+
chromium: raw => {
|
|
255
|
+
const m = raw.match(/^Chromium\s+([\d.]+)/);
|
|
256
|
+
if (!m) return null;
|
|
257
|
+
return { version: m[1], extra: [] };
|
|
258
|
+
},
|
|
259
|
+
// Mozilla Firefox 139.0
|
|
260
|
+
firefox: raw => {
|
|
261
|
+
const m = raw.match(/^Mozilla\s+Firefox\s+([\d.]+)/);
|
|
262
|
+
if (!m) return null;
|
|
263
|
+
return { version: m[1], extra: [] };
|
|
264
|
+
},
|
|
265
|
+
// Microsoft Edge 146.0.3856.84
|
|
266
|
+
msedge: raw => {
|
|
267
|
+
const m = raw.match(/^Microsoft\s+Edge\s+([\d.]+)/);
|
|
268
|
+
if (!m) return null;
|
|
269
|
+
return { version: m[1], extra: [] };
|
|
270
|
+
},
|
|
271
|
+
// deno 2.7.9 (stable, release, x86_64-unknown-linux-gnu)
|
|
272
|
+
deno: raw => {
|
|
273
|
+
const m = raw.match(/^deno\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
274
|
+
if (!m) return null;
|
|
275
|
+
const extra = m[2]
|
|
276
|
+
? m[2]
|
|
277
|
+
.split(',')
|
|
278
|
+
.map(s => s.trim())
|
|
279
|
+
.filter(Boolean)
|
|
280
|
+
: [];
|
|
281
|
+
return { version: m[1], extra };
|
|
282
|
+
},
|
|
283
|
+
// Version 1.58.2 (Playwright CLI)
|
|
284
|
+
playwright: raw => {
|
|
285
|
+
const m = raw.match(/(?:Version\s+)?([\d.]+)/);
|
|
286
|
+
if (!m) return null;
|
|
287
|
+
return { version: m[1], extra: [] };
|
|
288
|
+
},
|
|
289
|
+
// @playwright/test@1.58.2
|
|
290
|
+
playwrightTest: raw => {
|
|
291
|
+
const m = raw.match(/@playwright\/test@([\d.]+)/);
|
|
292
|
+
if (!m) return null;
|
|
293
|
+
return { version: m[1], extra: [] };
|
|
294
|
+
},
|
|
295
|
+
// @playwright/mcp@0.0.69 or `-- @playwright/mcp@0.0.69
|
|
296
|
+
playwrightMcp: raw => {
|
|
297
|
+
const m = raw.match(/@playwright\/mcp@([\d.]+)/);
|
|
298
|
+
if (!m) return null;
|
|
299
|
+
return { version: m[1], extra: [] };
|
|
300
|
+
},
|
|
301
|
+
// @puppeteer/browsers@2.13.0
|
|
302
|
+
puppeteerBrowsers: raw => {
|
|
303
|
+
const m = raw.match(/@puppeteer\/browsers@([\d.]+)/);
|
|
304
|
+
if (!m) return null;
|
|
305
|
+
return { version: m[1], extra: [] };
|
|
306
|
+
},
|
|
307
|
+
// 2.1.87 (Claude Code)
|
|
308
|
+
claudeCode: raw => {
|
|
309
|
+
const m = raw.match(/([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
310
|
+
if (!m) return null;
|
|
311
|
+
return { version: m[1], extra: m[2] ? [m[2]] : [] };
|
|
312
|
+
},
|
|
313
|
+
// GitHub Copilot CLI 1.0.14.\nRun 'copilot update'...
|
|
314
|
+
copilot: raw => {
|
|
315
|
+
const m = raw.match(/([\d.]+)/);
|
|
316
|
+
if (!m) return null;
|
|
317
|
+
// Strip trailing dot from version (e.g. "1.0.14." -> "1.0.14")
|
|
318
|
+
const version = m[1].replace(/\.$/, '');
|
|
319
|
+
return { version, extra: [] };
|
|
320
|
+
},
|
|
321
|
+
// pyenv 2.6.26
|
|
322
|
+
pyenv: raw => {
|
|
323
|
+
const m = raw.match(/^pyenv\s+([\d.]+)/);
|
|
324
|
+
if (!m) return null;
|
|
325
|
+
return { version: m[1], extra: [] };
|
|
326
|
+
},
|
|
327
|
+
// /workspace/.perl5/bin/perlbrew - App::perlbrew/1.02
|
|
328
|
+
perlbrew: raw => {
|
|
329
|
+
const m = raw.match(/App::perlbrew\/([\d.]+)/);
|
|
330
|
+
if (!m) return null;
|
|
331
|
+
return { version: m[1], extra: [] };
|
|
332
|
+
},
|
|
333
|
+
// rbenv 1.3.2-20-g23c3041
|
|
334
|
+
rbenv: raw => {
|
|
335
|
+
const m = raw.match(/^rbenv\s+([\d.]+(?:-[\w]+)*)/);
|
|
336
|
+
if (!m) return null;
|
|
337
|
+
return { version: m[1], extra: [] };
|
|
338
|
+
},
|
|
339
|
+
// Homebrew 5.1.2
|
|
340
|
+
brew: raw => {
|
|
341
|
+
const m = raw.match(/^Homebrew\s+([\d.]+)/);
|
|
342
|
+
if (!m) return null;
|
|
343
|
+
return { version: m[1], extra: [] };
|
|
344
|
+
},
|
|
345
|
+
// This is Zip 3.0 (July 5th 2008), by Info-ZIP.
|
|
346
|
+
zip: raw => {
|
|
347
|
+
const m = raw.match(/Zip\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
|
|
348
|
+
if (!m) return null;
|
|
349
|
+
return { version: m[1], extra: m[2] ? [m[2]] : [] };
|
|
350
|
+
},
|
|
351
|
+
// UnZip 6.00 of 20 April 2009, by Debian.
|
|
352
|
+
unzip: raw => {
|
|
353
|
+
const m = raw.match(/UnZip\s+([\d.]+)\s*(?:of\s+([^,]+))?/);
|
|
354
|
+
if (!m) return null;
|
|
355
|
+
return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
|
|
356
|
+
},
|
|
357
|
+
// ii xvfb 2:21.1.12-1ubuntu1.5 amd64 Virtual Framebuffer...
|
|
358
|
+
xvfb: raw => {
|
|
359
|
+
// dpkg output format
|
|
360
|
+
const dpkg = raw.match(/^ii\s+xvfb\s+(\S+)/);
|
|
361
|
+
if (dpkg) {
|
|
362
|
+
// Strip epoch (e.g. "2:21.1.12-1ubuntu1.5" -> "21.1.12-1ubuntu1.5")
|
|
363
|
+
const ver = dpkg[1].replace(/^\d+:/, '');
|
|
364
|
+
return { version: ver, extra: [] };
|
|
365
|
+
}
|
|
366
|
+
// X.Org X Server version output (if it ever works)
|
|
367
|
+
const xorg = raw.match(/X\.Org\s+X\s+Server\s+([\d.]+)/);
|
|
368
|
+
if (xorg) return { version: xorg[1], extra: [] };
|
|
369
|
+
return null;
|
|
370
|
+
},
|
|
371
|
+
// Xvfb returns "Unrecognized option: -version" — this is handled by fixing the command
|
|
372
|
+
// to use dpkg fallback first
|
|
373
|
+
|
|
374
|
+
// agent 1.0.0 or similar
|
|
375
|
+
agent: raw => {
|
|
376
|
+
const m = raw.match(/([\d.]+)/);
|
|
377
|
+
if (!m) return null;
|
|
378
|
+
return { version: m[1], extra: [] };
|
|
379
|
+
},
|
|
380
|
+
// codex-cli 0.117.0 or similar
|
|
381
|
+
codex: raw => {
|
|
382
|
+
const m = raw.match(/([\d.]+)/);
|
|
383
|
+
if (!m) return null;
|
|
384
|
+
return { version: m[1], extra: [] };
|
|
385
|
+
},
|
|
386
|
+
// opencode 1.3.10 or similar
|
|
387
|
+
opencode: raw => {
|
|
388
|
+
const m = raw.match(/([\d.]+)/);
|
|
389
|
+
if (!m) return null;
|
|
390
|
+
return { version: m[1], extra: [] };
|
|
391
|
+
},
|
|
392
|
+
// qwen-code version
|
|
393
|
+
qwenCode: raw => {
|
|
394
|
+
const m = raw.match(/([\d.]+)/);
|
|
395
|
+
if (!m) return null;
|
|
396
|
+
return { version: m[1], extra: [] };
|
|
397
|
+
},
|
|
398
|
+
// gemini version
|
|
399
|
+
gemini: raw => {
|
|
400
|
+
const m = raw.match(/([\d.]+)/);
|
|
401
|
+
if (!m) return null;
|
|
402
|
+
return { version: m[1], extra: [] };
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Parse a raw version string using the per-tool parser, returning uniform format:
|
|
408
|
+
* "<version>" or "<version> (<extra1>, <extra2>, ...)"
|
|
409
|
+
* Falls back to the raw string if no parser matches.
|
|
410
|
+
* @param {string} key - Tool key (must match a key in VERSION_PARSERS)
|
|
411
|
+
* @param {string} raw - Raw version string from command output
|
|
412
|
+
* @returns {string} Parsed version string in uniform format
|
|
413
|
+
*/
|
|
414
|
+
export function parseVersion(key, raw) {
|
|
415
|
+
if (!raw) return raw;
|
|
416
|
+
const parser = VERSION_PARSERS[key];
|
|
417
|
+
if (!parser) return raw;
|
|
418
|
+
const result = parser(raw);
|
|
419
|
+
if (!result) return raw;
|
|
420
|
+
const { version, extra } = result;
|
|
421
|
+
if (extra && extra.length > 0) {
|
|
422
|
+
return `${version} (${extra.join(', ')})`;
|
|
423
|
+
}
|
|
424
|
+
return version;
|
|
425
|
+
}
|
|
426
|
+
|
|
36
427
|
/**
|
|
37
428
|
* Command definitions for version checking
|
|
38
429
|
* Each entry has: key, command, and optional fallbacks
|
|
@@ -57,8 +448,8 @@ const VERSION_COMMANDS = [
|
|
|
57
448
|
|
|
58
449
|
// Browsers (installed via Playwright)
|
|
59
450
|
{ key: 'chrome', command: 'google-chrome --version 2>&1' },
|
|
60
|
-
{ key: 'chromium', command: 'chromium --version 2>&1', fallbacks: ['chromium-browser --version 2>&1'] },
|
|
61
|
-
{ key: 'firefox', command: 'firefox --version 2>&1' },
|
|
451
|
+
{ key: 'chromium', command: 'chromium --version 2>&1', fallbacks: ['chromium-browser --version 2>&1', "ls ~/.cache/ms-playwright/ 2>/dev/null | grep -oE 'chromium-[0-9]+' | head -1"] },
|
|
452
|
+
{ key: 'firefox', command: 'firefox --version 2>&1', fallbacks: ["ls ~/.cache/ms-playwright/ 2>/dev/null | grep -oE 'firefox-[0-9]+' | head -1"] },
|
|
62
453
|
{ key: 'msedge', command: 'microsoft-edge --version 2>&1', fallbacks: ['microsoft-edge-stable --version 2>&1'] },
|
|
63
454
|
{ key: 'webkit', command: "ls ~/.cache/ms-playwright/ 2>/dev/null | grep -oE 'webkit-[0-9]+' | head -1" },
|
|
64
455
|
|
|
@@ -109,7 +500,7 @@ const VERSION_COMMANDS = [
|
|
|
109
500
|
{ key: 'gpp', command: 'g++ --version 2>&1 | head -n1' },
|
|
110
501
|
{ key: 'clang', command: 'clang --version 2>&1 | head -n1' },
|
|
111
502
|
{ key: 'llvm', command: 'llvm-config --version 2>&1' },
|
|
112
|
-
{ key: 'lld', command: 'lld --version 2>&1 | head -n1' },
|
|
503
|
+
{ key: 'lld', command: 'ld.lld --version 2>&1 | head -n1', fallbacks: ['lld --version 2>&1 | head -n1'] },
|
|
113
504
|
{ key: 'make', command: 'make --version 2>&1 | head -n1' },
|
|
114
505
|
{ key: 'cmake', command: 'cmake --version 2>&1 | head -n1' },
|
|
115
506
|
|
|
@@ -139,7 +530,7 @@ const VERSION_COMMANDS = [
|
|
|
139
530
|
{ key: 'unzip', command: 'unzip -v 2>&1 | head -n1' },
|
|
140
531
|
{ key: 'expect', command: 'expect -version 2>&1' },
|
|
141
532
|
{ key: 'screen', command: 'screen --version 2>&1' },
|
|
142
|
-
{ key: 'xvfb', command: '
|
|
533
|
+
{ key: 'xvfb', command: 'dpkg -l xvfb 2>/dev/null | grep "^ii" | head -1', fallbacks: ['Xvfb -version 2>&1 | head -n1'] },
|
|
143
534
|
];
|
|
144
535
|
|
|
145
536
|
/**
|
|
@@ -353,14 +744,17 @@ export async function getVersionInfo(verbose = false, processVersion = null) {
|
|
|
353
744
|
}
|
|
354
745
|
|
|
355
746
|
/**
|
|
356
|
-
* Helper to add version line if version exists
|
|
747
|
+
* Helper to add version line if version exists.
|
|
748
|
+
* Uses parseVersion() to normalize raw output into uniform format.
|
|
357
749
|
* @param {string[]} lines - Array to push to
|
|
358
750
|
* @param {string} label - Display label
|
|
359
751
|
* @param {string|null} version - Version string or null
|
|
752
|
+
* @param {string} [key] - Tool key for version parser lookup
|
|
360
753
|
*/
|
|
361
|
-
function addVersionLine(lines, label, version) {
|
|
754
|
+
function addVersionLine(lines, label, version, key) {
|
|
362
755
|
if (version) {
|
|
363
|
-
|
|
756
|
+
const display = key ? parseVersion(key, version) : version;
|
|
757
|
+
lines.push(`• ${label}: \`${display}\``);
|
|
364
758
|
}
|
|
365
759
|
}
|
|
366
760
|
|
|
@@ -384,13 +778,13 @@ export function formatVersionMessage(versions) {
|
|
|
384
778
|
|
|
385
779
|
// === AI Agents (--tool options) ===
|
|
386
780
|
const agentLines = [];
|
|
387
|
-
addVersionLine(agentLines, 'Claude Code', versions.claudeCode);
|
|
388
|
-
addVersionLine(agentLines, 'Agent CLI', versions.agent);
|
|
389
|
-
addVersionLine(agentLines, 'OpenAI Codex', versions.codex);
|
|
390
|
-
addVersionLine(agentLines, 'OpenCode', versions.opencode);
|
|
391
|
-
addVersionLine(agentLines, 'Qwen Code', versions.qwenCode);
|
|
392
|
-
addVersionLine(agentLines, 'Gemini CLI', versions.gemini);
|
|
393
|
-
addVersionLine(agentLines, 'GitHub Copilot', versions.copilot);
|
|
781
|
+
addVersionLine(agentLines, 'Claude Code', versions.claudeCode, 'claudeCode');
|
|
782
|
+
addVersionLine(agentLines, 'Agent CLI', versions.agent, 'agent');
|
|
783
|
+
addVersionLine(agentLines, 'OpenAI Codex', versions.codex, 'codex');
|
|
784
|
+
addVersionLine(agentLines, 'OpenCode', versions.opencode, 'opencode');
|
|
785
|
+
addVersionLine(agentLines, 'Qwen Code', versions.qwenCode, 'qwenCode');
|
|
786
|
+
addVersionLine(agentLines, 'Gemini CLI', versions.gemini, 'gemini');
|
|
787
|
+
addVersionLine(agentLines, 'GitHub Copilot', versions.copilot, 'copilot');
|
|
394
788
|
|
|
395
789
|
if (agentLines.length > 0) {
|
|
396
790
|
lines.push('');
|
|
@@ -402,7 +796,7 @@ export function formatVersionMessage(versions) {
|
|
|
402
796
|
const jsLines = [];
|
|
403
797
|
addVersionLine(jsLines, 'Node.js', versions.node);
|
|
404
798
|
addVersionLine(jsLines, 'Bun', versions.bun);
|
|
405
|
-
addVersionLine(jsLines, 'Deno', versions.deno);
|
|
799
|
+
addVersionLine(jsLines, 'Deno', versions.deno, 'deno');
|
|
406
800
|
addVersionLine(jsLines, 'NPM', versions.npm);
|
|
407
801
|
addVersionLine(jsLines, 'NVM', versions.nvm);
|
|
408
802
|
|
|
@@ -414,8 +808,8 @@ export function formatVersionMessage(versions) {
|
|
|
414
808
|
|
|
415
809
|
// === Python ===
|
|
416
810
|
const pythonLines = [];
|
|
417
|
-
addVersionLine(pythonLines, 'Python', versions.python);
|
|
418
|
-
addVersionLine(pythonLines, 'Pyenv', versions.pyenv);
|
|
811
|
+
addVersionLine(pythonLines, 'Python', versions.python, 'python');
|
|
812
|
+
addVersionLine(pythonLines, 'Pyenv', versions.pyenv, 'pyenv');
|
|
419
813
|
|
|
420
814
|
if (pythonLines.length > 0) {
|
|
421
815
|
lines.push('');
|
|
@@ -425,8 +819,8 @@ export function formatVersionMessage(versions) {
|
|
|
425
819
|
|
|
426
820
|
// === Rust ===
|
|
427
821
|
const rustLines = [];
|
|
428
|
-
addVersionLine(rustLines, 'Rustc', versions.rust);
|
|
429
|
-
addVersionLine(rustLines, 'Cargo', versions.cargo);
|
|
822
|
+
addVersionLine(rustLines, 'Rustc', versions.rust, 'rust');
|
|
823
|
+
addVersionLine(rustLines, 'Cargo', versions.cargo, 'cargo');
|
|
430
824
|
|
|
431
825
|
if (rustLines.length > 0) {
|
|
432
826
|
lines.push('');
|
|
@@ -436,7 +830,7 @@ export function formatVersionMessage(versions) {
|
|
|
436
830
|
|
|
437
831
|
// === Java ===
|
|
438
832
|
const javaLines = [];
|
|
439
|
-
addVersionLine(javaLines, 'Java', versions.java);
|
|
833
|
+
addVersionLine(javaLines, 'Java', versions.java, 'java');
|
|
440
834
|
addVersionLine(javaLines, 'SDKMAN', versions.sdkman);
|
|
441
835
|
|
|
442
836
|
if (javaLines.length > 0) {
|
|
@@ -449,14 +843,14 @@ export function formatVersionMessage(versions) {
|
|
|
449
843
|
if (versions.go) {
|
|
450
844
|
lines.push('');
|
|
451
845
|
lines.push('*🔷 Go*');
|
|
452
|
-
addVersionLine(lines, 'Go', versions.go);
|
|
846
|
+
addVersionLine(lines, 'Go', versions.go, 'go');
|
|
453
847
|
}
|
|
454
848
|
|
|
455
849
|
// === PHP ===
|
|
456
850
|
if (versions.php) {
|
|
457
851
|
lines.push('');
|
|
458
852
|
lines.push('*🐘 PHP*');
|
|
459
|
-
addVersionLine(lines, 'PHP', versions.php);
|
|
853
|
+
addVersionLine(lines, 'PHP', versions.php, 'php');
|
|
460
854
|
}
|
|
461
855
|
|
|
462
856
|
// === .NET ===
|
|
@@ -469,7 +863,7 @@ export function formatVersionMessage(versions) {
|
|
|
469
863
|
// === Perl ===
|
|
470
864
|
const perlLines = [];
|
|
471
865
|
addVersionLine(perlLines, 'Perl', versions.perl);
|
|
472
|
-
addVersionLine(perlLines, 'Perlbrew', versions.perlbrew);
|
|
866
|
+
addVersionLine(perlLines, 'Perlbrew', versions.perlbrew, 'perlbrew');
|
|
473
867
|
|
|
474
868
|
if (perlLines.length > 0) {
|
|
475
869
|
lines.push('');
|
|
@@ -479,9 +873,9 @@ export function formatVersionMessage(versions) {
|
|
|
479
873
|
|
|
480
874
|
// === OCaml/Rocq ===
|
|
481
875
|
const ocamlLines = [];
|
|
482
|
-
addVersionLine(ocamlLines, 'OCaml', versions.ocaml);
|
|
876
|
+
addVersionLine(ocamlLines, 'OCaml', versions.ocaml, 'ocaml');
|
|
483
877
|
addVersionLine(ocamlLines, 'Opam', versions.opam);
|
|
484
|
-
addVersionLine(ocamlLines, 'Rocq/Coq', versions.rocq);
|
|
878
|
+
addVersionLine(ocamlLines, 'Rocq/Coq', versions.rocq, 'rocq');
|
|
485
879
|
|
|
486
880
|
if (ocamlLines.length > 0) {
|
|
487
881
|
lines.push('');
|
|
@@ -491,8 +885,8 @@ export function formatVersionMessage(versions) {
|
|
|
491
885
|
|
|
492
886
|
// === Lean ===
|
|
493
887
|
const leanLines = [];
|
|
494
|
-
addVersionLine(leanLines, 'Lean', versions.lean);
|
|
495
|
-
addVersionLine(leanLines, 'Elan', versions.elan);
|
|
888
|
+
addVersionLine(leanLines, 'Lean', versions.lean, 'lean');
|
|
889
|
+
addVersionLine(leanLines, 'Elan', versions.elan, 'elan');
|
|
496
890
|
addVersionLine(leanLines, 'Lake', versions.lake);
|
|
497
891
|
|
|
498
892
|
if (leanLines.length > 0) {
|
|
@@ -503,8 +897,8 @@ export function formatVersionMessage(versions) {
|
|
|
503
897
|
|
|
504
898
|
// === Ruby ===
|
|
505
899
|
const rubyLines = [];
|
|
506
|
-
addVersionLine(rubyLines, 'Ruby', versions.ruby);
|
|
507
|
-
addVersionLine(rubyLines, 'Rbenv', versions.rbenv);
|
|
900
|
+
addVersionLine(rubyLines, 'Ruby', versions.ruby, 'ruby');
|
|
901
|
+
addVersionLine(rubyLines, 'Rbenv', versions.rbenv, 'rbenv');
|
|
508
902
|
|
|
509
903
|
if (rubyLines.length > 0) {
|
|
510
904
|
lines.push('');
|
|
@@ -516,34 +910,34 @@ export function formatVersionMessage(versions) {
|
|
|
516
910
|
if (versions.kotlin) {
|
|
517
911
|
lines.push('');
|
|
518
912
|
lines.push('*🟣 Kotlin*');
|
|
519
|
-
addVersionLine(lines, 'Kotlin', versions.kotlin);
|
|
913
|
+
addVersionLine(lines, 'Kotlin', versions.kotlin, 'kotlin');
|
|
520
914
|
}
|
|
521
915
|
|
|
522
916
|
// === Swift ===
|
|
523
917
|
if (versions.swift) {
|
|
524
918
|
lines.push('');
|
|
525
919
|
lines.push('*🦅 Swift*');
|
|
526
|
-
addVersionLine(lines, 'Swift', versions.swift);
|
|
920
|
+
addVersionLine(lines, 'Swift', versions.swift, 'swift');
|
|
527
921
|
}
|
|
528
922
|
|
|
529
923
|
// === R ===
|
|
530
924
|
if (versions.r) {
|
|
531
925
|
lines.push('');
|
|
532
926
|
lines.push('*📊 R*');
|
|
533
|
-
addVersionLine(lines, 'R', versions.r);
|
|
927
|
+
addVersionLine(lines, 'R', versions.r, 'r');
|
|
534
928
|
}
|
|
535
929
|
|
|
536
930
|
// === C/C++ ===
|
|
537
931
|
const cppLines = [];
|
|
538
|
-
addVersionLine(cppLines, 'GCC', versions.gcc);
|
|
539
|
-
addVersionLine(cppLines, 'G++', versions.gpp);
|
|
540
|
-
addVersionLine(cppLines, 'Clang', versions.clang);
|
|
932
|
+
addVersionLine(cppLines, 'GCC', versions.gcc, 'gcc');
|
|
933
|
+
addVersionLine(cppLines, 'G++', versions.gpp, 'gpp');
|
|
934
|
+
addVersionLine(cppLines, 'Clang', versions.clang, 'clang');
|
|
541
935
|
addVersionLine(cppLines, 'LLVM', versions.llvm);
|
|
542
|
-
addVersionLine(cppLines, 'LLD', versions.lld);
|
|
543
|
-
addVersionLine(cppLines, 'Make', versions.make);
|
|
544
|
-
addVersionLine(cppLines, 'CMake', versions.cmake);
|
|
545
|
-
addVersionLine(cppLines, 'NASM', versions.nasm);
|
|
546
|
-
addVersionLine(cppLines, 'FASM', versions.fasm);
|
|
936
|
+
addVersionLine(cppLines, 'LLD', versions.lld, 'lld');
|
|
937
|
+
addVersionLine(cppLines, 'Make', versions.make, 'make');
|
|
938
|
+
addVersionLine(cppLines, 'CMake', versions.cmake, 'cmake');
|
|
939
|
+
addVersionLine(cppLines, 'NASM', versions.nasm, 'nasm');
|
|
940
|
+
addVersionLine(cppLines, 'FASM', versions.fasm, 'fasm');
|
|
547
941
|
|
|
548
942
|
if (cppLines.length > 0) {
|
|
549
943
|
lines.push('');
|
|
@@ -553,10 +947,10 @@ export function formatVersionMessage(versions) {
|
|
|
553
947
|
|
|
554
948
|
// === Browsers ===
|
|
555
949
|
const browserLines = [];
|
|
556
|
-
addVersionLine(browserLines, 'Google Chrome', versions.chrome);
|
|
557
|
-
addVersionLine(browserLines, 'Chromium', versions.chromium);
|
|
558
|
-
addVersionLine(browserLines, 'Firefox', versions.firefox);
|
|
559
|
-
addVersionLine(browserLines, 'Microsoft Edge', versions.msedge);
|
|
950
|
+
addVersionLine(browserLines, 'Google Chrome', versions.chrome, 'chrome');
|
|
951
|
+
addVersionLine(browserLines, 'Chromium', versions.chromium, 'chromium');
|
|
952
|
+
addVersionLine(browserLines, 'Firefox', versions.firefox, 'firefox');
|
|
953
|
+
addVersionLine(browserLines, 'Microsoft Edge', versions.msedge, 'msedge');
|
|
560
954
|
addVersionLine(browserLines, 'WebKit', versions.webkit);
|
|
561
955
|
|
|
562
956
|
if (browserLines.length > 0) {
|
|
@@ -567,15 +961,15 @@ export function formatVersionMessage(versions) {
|
|
|
567
961
|
|
|
568
962
|
// === Browser Automation ===
|
|
569
963
|
const browserAutoLines = [];
|
|
570
|
-
addVersionLine(browserAutoLines, 'Playwright', versions.playwright);
|
|
571
|
-
addVersionLine(browserAutoLines, 'Playwright Test', versions.playwrightTest);
|
|
572
|
-
|
|
573
|
-
if (versions.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
browserAutoLines.push(
|
|
964
|
+
addVersionLine(browserAutoLines, 'Playwright', versions.playwright, 'playwright');
|
|
965
|
+
addVersionLine(browserAutoLines, 'Playwright Test', versions.playwrightTest, 'playwrightTest');
|
|
966
|
+
// Playwright MCP: show version with Claude Code connection status inline
|
|
967
|
+
if (versions.playwrightMcp) {
|
|
968
|
+
const mcpVersion = parseVersion('playwrightMcp', versions.playwrightMcp);
|
|
969
|
+
const claudeStatus = versions.playwrightMcpStatus ? 'connected' : 'not connected';
|
|
970
|
+
browserAutoLines.push(`• Playwright MCP: \`${mcpVersion} | Claude Code: ${claudeStatus}\``);
|
|
577
971
|
}
|
|
578
|
-
addVersionLine(browserAutoLines, 'Puppeteer Browsers', versions.puppeteerBrowsers);
|
|
972
|
+
addVersionLine(browserAutoLines, 'Puppeteer Browsers', versions.puppeteerBrowsers, 'puppeteerBrowsers');
|
|
579
973
|
|
|
580
974
|
if (browserAutoLines.length > 0) {
|
|
581
975
|
lines.push('');
|
|
@@ -585,17 +979,17 @@ export function formatVersionMessage(versions) {
|
|
|
585
979
|
|
|
586
980
|
// === Development Tools ===
|
|
587
981
|
const toolLines = [];
|
|
588
|
-
addVersionLine(toolLines, 'Git', versions.git);
|
|
589
|
-
addVersionLine(toolLines, 'GitHub CLI', versions.gh);
|
|
590
|
-
addVersionLine(toolLines, 'GitLab CLI', versions.glab);
|
|
591
|
-
addVersionLine(toolLines, 'Homebrew', versions.brew);
|
|
592
|
-
addVersionLine(toolLines, 'cURL', versions.curl);
|
|
593
|
-
addVersionLine(toolLines, 'Wget', versions.wget);
|
|
594
|
-
addVersionLine(toolLines, 'Zip', versions.zip);
|
|
595
|
-
addVersionLine(toolLines, 'Unzip', versions.unzip);
|
|
596
|
-
addVersionLine(toolLines, 'Expect', versions.expect);
|
|
597
|
-
addVersionLine(toolLines, 'Screen', versions.screen);
|
|
598
|
-
addVersionLine(toolLines, 'Xvfb', versions.xvfb);
|
|
982
|
+
addVersionLine(toolLines, 'Git', versions.git, 'git');
|
|
983
|
+
addVersionLine(toolLines, 'GitHub CLI', versions.gh, 'gh');
|
|
984
|
+
addVersionLine(toolLines, 'GitLab CLI', versions.glab, 'glab');
|
|
985
|
+
addVersionLine(toolLines, 'Homebrew', versions.brew, 'brew');
|
|
986
|
+
addVersionLine(toolLines, 'cURL', versions.curl, 'curl');
|
|
987
|
+
addVersionLine(toolLines, 'Wget', versions.wget, 'wget');
|
|
988
|
+
addVersionLine(toolLines, 'Zip', versions.zip, 'zip');
|
|
989
|
+
addVersionLine(toolLines, 'Unzip', versions.unzip, 'unzip');
|
|
990
|
+
addVersionLine(toolLines, 'Expect', versions.expect, 'expect');
|
|
991
|
+
addVersionLine(toolLines, 'Screen', versions.screen, 'screen');
|
|
992
|
+
addVersionLine(toolLines, 'Xvfb', versions.xvfb, 'xvfb');
|
|
599
993
|
|
|
600
994
|
if (toolLines.length > 0) {
|
|
601
995
|
lines.push('');
|
|
@@ -616,4 +1010,5 @@ export function formatVersionMessage(versions) {
|
|
|
616
1010
|
export default {
|
|
617
1011
|
getVersionInfo,
|
|
618
1012
|
formatVersionMessage,
|
|
1013
|
+
parseVersion,
|
|
619
1014
|
};
|