@link-assistant/hive-mind 1.46.2 → 1.46.4

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.46.4
4
+
5
+ ### Patch Changes
6
+
7
+ - a3bdea6: Fix CI/CD false positive for .gitkeep files using positive matching (Issue #1528).
8
+
9
+ Use consistent positive matching in detect-code-changes.mjs: "Files considered as code changes" now only shows files matching codePattern, so unknown file types like .gitkeep are naturally excluded without explicit exclusion rules. Add 40 unit tests covering the full detection pipeline.
10
+
11
+ ## 1.46.3
12
+
13
+ ### Patch Changes
14
+
15
+ - c425744: Standardize /version output — strip OS/arch, normalize dates, enhance platform detection (Issue #1524)
16
+ - Strip OS/architecture info (e.g. x86_64-unknown-linux-gnu, linux/amd64) from version strings for cleaner output
17
+ - Normalize date formats to ISO (YYYY-MM-DD) across all version components
18
+ - Enhance platform detection for consistent environment reporting
19
+
3
20
  ## 1.46.2
4
21
 
5
22
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.46.2",
3
+ "version": "1.46.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -13,6 +13,89 @@ import { promisify } from 'util';
13
13
 
14
14
  const execAsync = promisify(exec);
15
15
 
16
+ /**
17
+ * Month name/abbreviation to zero-padded number mapping
18
+ */
19
+ const MONTH_MAP = {
20
+ jan: '01',
21
+ january: '01',
22
+ feb: '02',
23
+ february: '02',
24
+ mar: '03',
25
+ march: '03',
26
+ apr: '04',
27
+ april: '04',
28
+ may: '05',
29
+ jun: '06',
30
+ june: '06',
31
+ jul: '07',
32
+ july: '07',
33
+ aug: '08',
34
+ august: '08',
35
+ sep: '09',
36
+ september: '09',
37
+ oct: '10',
38
+ october: '10',
39
+ nov: '11',
40
+ november: '11',
41
+ dec: '12',
42
+ december: '12',
43
+ };
44
+
45
+ /**
46
+ * Normalize a date string to ISO format (YYYY-MM-DD).
47
+ * Handles formats like:
48
+ * "20-Aug-23" → "2023-08-20"
49
+ * "20 April 2009" → "2009-04-20"
50
+ * "July 5th 2008" → "2008-07-05"
51
+ * "Jan 13 2026" → "2026-01-13"
52
+ * "2024-02-29" → "2024-02-29" (passthrough)
53
+ * Returns the original string if parsing fails.
54
+ * @param {string} dateStr - Date string to normalize
55
+ * @returns {string} ISO date string or original
56
+ */
57
+ export function normalizeDate(dateStr) {
58
+ if (!dateStr) return dateStr;
59
+ const s = dateStr.trim();
60
+
61
+ // Already ISO: YYYY-MM-DD
62
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
63
+
64
+ // "DD-Mon-YY" (e.g. "20-Aug-23")
65
+ const dmy = s.match(/^(\d{1,2})-([A-Za-z]{3})-(\d{2})$/);
66
+ if (dmy) {
67
+ const month = MONTH_MAP[dmy[2].toLowerCase()];
68
+ if (month) {
69
+ const year = parseInt(dmy[3], 10);
70
+ const fullYear = year >= 70 ? `19${dmy[3]}` : `20${dmy[3]}`;
71
+ return `${fullYear}-${month}-${dmy[1].padStart(2, '0')}`;
72
+ }
73
+ }
74
+
75
+ // "DD Month YYYY" (e.g. "20 April 2009")
76
+ const dmY = s.match(/^(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})$/);
77
+ if (dmY) {
78
+ const month = MONTH_MAP[dmY[2].toLowerCase()];
79
+ if (month) return `${dmY[3]}-${month}-${dmY[1].padStart(2, '0')}`;
80
+ }
81
+
82
+ // "Month DDth YYYY" or "Month DD YYYY" (e.g. "July 5th 2008", "Jan 13 2026")
83
+ const mdY = s.match(/^([A-Za-z]+)\s+(\d{1,2})(?:st|nd|rd|th)?\s+(\d{4})$/);
84
+ if (mdY) {
85
+ const month = MONTH_MAP[mdY[1].toLowerCase()];
86
+ if (month) return `${mdY[3]}-${month}-${mdY[2].padStart(2, '0')}`;
87
+ }
88
+
89
+ // "Month DD YYYY HH:MM:SS" (e.g. "Jan 13 2026 22:36:55")
90
+ const mdYt = s.match(/^([A-Za-z]+)\s+(\d{1,2})\s+(\d{4})\s+(\d{2}:\d{2}:\d{2})$/);
91
+ if (mdYt) {
92
+ const month = MONTH_MAP[mdYt[1].toLowerCase()];
93
+ if (month) return `${mdYt[3]}-${month}-${mdYt[2].padStart(2, '0')} ${mdYt[4]}`;
94
+ }
95
+
96
+ return s;
97
+ }
98
+
16
99
  /**
17
100
  * Execute a command asynchronously and return its output, or null if it fails
18
101
  * @param {string} command - Command to execute
@@ -58,19 +141,30 @@ const VERSION_PARSERS = {
58
141
  const extra = m[2] ? m[2].trim().split(/\s+/) : [];
59
142
  return { version: m[1], extra };
60
143
  },
61
- // go version go1.26.1 linux/amd64
144
+ // go version go1.26.1 linux/amd64 → strip platform/arch
62
145
  go: raw => {
63
- const m = raw.match(/go([\d.]+(?:\S*)?)\s+(.*)/);
146
+ const m = raw.match(/go([\d.]+(?:\S*)?)/);
64
147
  if (!m) return null;
65
- return { version: m[1], extra: [m[2].trim()] };
148
+ return { version: m[1], extra: [] };
66
149
  },
67
- // PHP 8.3.30 (cli) (built: Jan 13 2026 22:36:55) (NTS)
150
+ // PHP 8.3.30 (cli) (built: Jan 13 2026 22:36:55) (NTS) → strip cli, normalize date
68
151
  php: raw => {
69
152
  const m = raw.match(/^PHP\s+([\d.]+(?:-\S+)?)\s*(.*)/);
70
153
  if (!m) return null;
71
154
  const tags = [];
72
155
  const parts = m[2].matchAll(/\(([^)]+)\)/g);
73
- for (const p of parts) tags.push(p[1]);
156
+ for (const p of parts) {
157
+ const tag = p[1].trim();
158
+ // Skip "cli" — not meaningful for version display
159
+ if (tag === 'cli') continue;
160
+ // Normalize "built: Jan 13 2026 22:36:55" → "2026-01-13 22:36:55"
161
+ const built = tag.match(/^built:\s+(.+)$/);
162
+ if (built) {
163
+ tags.push(normalizeDate(built[1]));
164
+ continue;
165
+ }
166
+ tags.push(tag);
167
+ }
74
168
  return { version: m[1], extra: tags };
75
169
  },
76
170
  // openjdk version "21" 2023-09-19 LTS
@@ -79,27 +173,32 @@ const VERSION_PARSERS = {
79
173
  if (!m) return null;
80
174
  return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
81
175
  },
82
- // gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 use full distro version
176
+ // gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 use base version only
83
177
  gcc: raw => {
84
- const m = raw.match(/^gcc\s+(?:\((\S+)\s+([\d.]+\S*)\)\s+)?([\d.]+)/);
178
+ const m = raw.match(/^gcc\s+(?:\([^)]*\)\s+)?([\d.]+)/);
85
179
  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: [] };
180
+ return { version: m[1], extra: [] };
89
181
  },
90
- // g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 use full distro version
182
+ // g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 use base version only
91
183
  gpp: raw => {
92
- const m = raw.match(/^g\+\+\s+(?:\((\S+)\s+([\d.]+\S*)\)\s+)?([\d.]+)/);
184
+ const m = raw.match(/^g\+\+\s+(?:\([^)]*\)\s+)?([\d.]+)/);
93
185
  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: [] };
186
+ return { version: m[1], extra: [] };
97
187
  },
98
- // clang version 17.0.0 (https://github.com/... commit)
188
+ // clang version 17.0.0 (https://github.com/... 2e6139970eda) → strip URL, keep commit
99
189
  clang: raw => {
100
190
  const m = raw.match(/^clang\s+version\s+([\d.]+(?:-\S+)?)\s*(?:\(([^)]+)\))?/);
101
191
  if (!m) return null;
102
- return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
192
+ if (m[2]) {
193
+ // Remove URLs, keep only hex commit hashes
194
+ const parts = m[2]
195
+ .trim()
196
+ .split(/\s+/)
197
+ .filter(p => !p.includes('://') && !p.includes('.git'));
198
+ const commitParts = parts.filter(p => /^[0-9a-f]{7,}$/i.test(p));
199
+ return { version: m[1], extra: commitParts };
200
+ }
201
+ return { version: m[1], extra: [] };
103
202
  },
104
203
  // LLD 17.0.0 (compatible with GNU linkers) — only version number matters
105
204
  lld: raw => {
@@ -113,34 +212,47 @@ const VERSION_PARSERS = {
113
212
  if (!m) return null;
114
213
  return { version: m[1], extra: [] };
115
214
  },
116
- // ruby 3.4.9 (2026-03-11 revision 76cca827ab) +PRISM [x86_64-linux]
215
+ // ruby 3.4.9 (2026-03-11 revision 76cca827ab) +PRISM [x86_64-linux] → strip arch, reformat
117
216
  ruby: raw => {
118
217
  const m = raw.match(/^ruby\s+([\d.]+(?:p\d+)?)\s*(?:\(([^)]+)\))?\s*(.*)/);
119
218
  if (!m) return null;
120
219
  const extra = [];
121
- if (m[2]) extra.push(m[2].trim());
220
+ if (m[2]) {
221
+ // Parse "2026-03-11 revision 76cca827ab" → commit, date
222
+ const revMatch = m[2].match(/^(\d{4}-\d{2}-\d{2})\s+revision\s+(\w+)$/);
223
+ if (revMatch) {
224
+ extra.push(revMatch[2]); // commit first
225
+ extra.push(revMatch[1]); // then date
226
+ } else {
227
+ extra.push(m[2].trim());
228
+ }
229
+ }
230
+ // Capture +PRISM or similar flags, but strip [arch] info
122
231
  const tail = m[3] ? m[3].trim() : '';
123
- if (tail) extra.push(tail);
232
+ if (tail) {
233
+ const cleaned = tail.replace(/\[[\w-]+\]/g, '').trim();
234
+ if (cleaned) extra.push(cleaned);
235
+ }
124
236
  return { version: m[1], extra };
125
237
  },
126
- // Kotlin version 2.3.20-release-208 (JRE 21+35-LTS)
238
+ // Kotlin version 2.3.20-release-208 (JRE 21+35-LTS) → strip -release-NNN suffix
127
239
  kotlin: raw => {
128
- const m = raw.match(/^Kotlin\s+version\s+([\d.\-\w]+)\s*(?:\(([^)]+)\))?/);
240
+ const m = raw.match(/^Kotlin\s+version\s+([\d.]+)(?:-release-\d+)?\s*(?:\(([^)]+)\))?/);
129
241
  if (!m) return null;
130
242
  return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
131
243
  },
132
- // Swift version 6.0.3 (swift-6.0.3-RELEASE)
244
+ // Swift version 6.0.3 (swift-6.0.3-RELEASE) → strip redundant release tag
133
245
  swift: raw => {
134
- const m = raw.match(/^Swift\s+version\s+([\d.]+(?:\.\d+)?)\s*(?:\(([^)]+)\))?/);
246
+ const m = raw.match(/^Swift\s+version\s+([\d.]+(?:\.\d+)?)/);
135
247
  if (!m) return null;
136
- return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
248
+ return { version: m[1], extra: [] };
137
249
  },
138
- // R version 4.3.3 (2024-02-29) -- "Angel Food Cake"
250
+ // R version 4.3.3 (2024-02-29) -- "Angel Food Cake" → normalize date
139
251
  r: raw => {
140
252
  const m = raw.match(/^R\s+version\s+([\d.]+)\s*(?:\(([^)]+)\))?(?:\s+--\s+"([^"]+)")?/);
141
253
  if (!m) return null;
142
254
  const extra = [];
143
- if (m[2]) extra.push(m[2]);
255
+ if (m[2]) extra.push(normalizeDate(m[2]));
144
256
  if (m[3]) extra.push(m[3]);
145
257
  return { version: m[1], extra };
146
258
  },
@@ -162,11 +274,11 @@ const VERSION_PARSERS = {
162
274
  if (!m) return null;
163
275
  return { version: m[1], extra: [] };
164
276
  },
165
- // curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 ...
277
+ // curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 ... → strip arch info
166
278
  curl: raw => {
167
- const m = raw.match(/^curl\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
279
+ const m = raw.match(/^curl\s+([\d.]+)/);
168
280
  if (!m) return null;
169
- return { version: m[1], extra: m[2] ? [m[2]] : [] };
281
+ return { version: m[1], extra: [] };
170
282
  },
171
283
  // GNU Wget 1.21.4 built on linux-gnu.
172
284
  wget: raw => {
@@ -198,13 +310,13 @@ const VERSION_PARSERS = {
198
310
  if (!m) return null;
199
311
  return { version: m[1], extra: [] };
200
312
  },
201
- // Screen version 4.09.01 (GNU) 20-Aug-23
313
+ // Screen version 4.09.01 (GNU) 20-Aug-23 → normalize date, strip GNU
202
314
  screen: raw => {
203
- const m = raw.match(/^Screen\s+version\s+([\d.]+)\s*(?:\(([^)]+)\))?\s*(.*)/);
315
+ const m = raw.match(/^Screen\s+version\s+([\d.]+)\s*(?:\([^)]*\))?\s*(.*)/);
204
316
  if (!m) return null;
205
317
  const extra = [];
206
- if (m[2]) extra.push(m[2]);
207
- if (m[3] && m[3].trim()) extra.push(m[3].trim());
318
+ const dateStr = m[2] ? m[2].trim() : '';
319
+ if (dateStr) extra.push(normalizeDate(dateStr));
208
320
  return { version: m[1], extra };
209
321
  },
210
322
  // expect version 5.45.4
@@ -232,7 +344,7 @@ const VERSION_PARSERS = {
232
344
  const extra = m[2] ? m[2].trim().split(/\s+/) : [];
233
345
  return { version: m[1], extra };
234
346
  },
235
- // Lean (version 4.29.0, x86_64-unknown-linux-gnu, commit abc123, Release)
347
+ // Lean (version 4.29.0, x86_64-unknown-linux-gnu, commit abc123, Release) → strip arch/Release
236
348
  lean: raw => {
237
349
  const m = raw.match(/version\s+([\d.]+)(?:,\s*(.+?))\)?$/);
238
350
  if (!m) return null;
@@ -240,7 +352,13 @@ const VERSION_PARSERS = {
240
352
  ? m[2]
241
353
  .split(',')
242
354
  .map(s => s.trim().replace(/\)$/, ''))
243
- .filter(Boolean)
355
+ .filter(s => {
356
+ if (!s) return false;
357
+ // Strip arch patterns and "Release"
358
+ if (/^\w+[-_]\w+[-_]\w+[-_]\w+$/.test(s)) return false;
359
+ if (s === 'Release') return false;
360
+ return true;
361
+ })
244
362
  : [];
245
363
  return { version: m[1], extra };
246
364
  },
@@ -268,7 +386,7 @@ const VERSION_PARSERS = {
268
386
  if (!m) return null;
269
387
  return { version: m[1], extra: [] };
270
388
  },
271
- // deno 2.7.9 (stable, release, x86_64-unknown-linux-gnu)
389
+ // deno 2.7.9 (stable, release, x86_64-unknown-linux-gnu) → keep only channel (stable)
272
390
  deno: raw => {
273
391
  const m = raw.match(/^deno\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
274
392
  if (!m) return null;
@@ -276,7 +394,7 @@ const VERSION_PARSERS = {
276
394
  ? m[2]
277
395
  .split(',')
278
396
  .map(s => s.trim())
279
- .filter(Boolean)
397
+ .filter(s => s && s !== 'release' && !s.includes('-') && !s.includes('/'))
280
398
  : [];
281
399
  return { version: m[1], extra };
282
400
  },
@@ -304,11 +422,11 @@ const VERSION_PARSERS = {
304
422
  if (!m) return null;
305
423
  return { version: m[1], extra: [] };
306
424
  },
307
- // 2.1.87 (Claude Code)
425
+ // 2.1.87 (Claude Code) → strip redundant product name
308
426
  claudeCode: raw => {
309
- const m = raw.match(/([\d.]+)\s*(?:\(([^)]+)\))?/);
427
+ const m = raw.match(/([\d.]+)/);
310
428
  if (!m) return null;
311
- return { version: m[1], extra: m[2] ? [m[2]] : [] };
429
+ return { version: m[1], extra: [] };
312
430
  },
313
431
  // GitHub Copilot CLI 1.0.14.\nRun 'copilot update'...
314
432
  copilot: raw => {
@@ -342,25 +460,26 @@ const VERSION_PARSERS = {
342
460
  if (!m) return null;
343
461
  return { version: m[1], extra: [] };
344
462
  },
345
- // This is Zip 3.0 (July 5th 2008), by Info-ZIP.
463
+ // This is Zip 3.0 (July 5th 2008), by Info-ZIP. → normalize date
346
464
  zip: raw => {
347
465
  const m = raw.match(/Zip\s+([\d.]+)\s*(?:\(([^)]+)\))?/);
348
466
  if (!m) return null;
349
- return { version: m[1], extra: m[2] ? [m[2]] : [] };
467
+ return { version: m[1], extra: m[2] ? [normalizeDate(m[2])] : [] };
350
468
  },
351
- // UnZip 6.00 of 20 April 2009, by Debian.
469
+ // UnZip 6.00 of 20 April 2009, by Debian. → normalize date
352
470
  unzip: raw => {
353
471
  const m = raw.match(/UnZip\s+([\d.]+)\s*(?:of\s+([^,]+))?/);
354
472
  if (!m) return null;
355
- return { version: m[1], extra: m[2] ? [m[2].trim()] : [] };
473
+ return { version: m[1], extra: m[2] ? [normalizeDate(m[2].trim())] : [] };
356
474
  },
357
- // ii xvfb 2:21.1.12-1ubuntu1.5 amd64 Virtual Framebuffer...
475
+ // ii xvfb 2:21.1.12-1ubuntu1.5 amd64 Virtual Framebuffer... → base version only
358
476
  xvfb: raw => {
359
477
  // dpkg output format
360
478
  const dpkg = raw.match(/^ii\s+xvfb\s+(\S+)/);
361
479
  if (dpkg) {
362
480
  // Strip epoch (e.g. "2:21.1.12-1ubuntu1.5" -> "21.1.12-1ubuntu1.5")
363
- const ver = dpkg[1].replace(/^\d+:/, '');
481
+ // Then strip distro suffix (e.g. "21.1.12-1ubuntu1.5" -> "21.1.12")
482
+ const ver = dpkg[1].replace(/^\d+:/, '').replace(/-.*$/, '');
364
483
  return { version: ver, extra: [] };
365
484
  }
366
485
  // X.Org X Server version output (if it ever works)
@@ -559,6 +678,95 @@ async function executeVersionCommand(cmdDef, verbose) {
559
678
  return { key: cmdDef.key, value: result };
560
679
  }
561
680
 
681
+ /**
682
+ * Map of process.arch values to human-friendly architecture names
683
+ */
684
+ const ARCH_NAMES = {
685
+ x64: 'AMD64 (x86-64)',
686
+ arm64: 'ARM64 (aarch64)',
687
+ arm: 'ARM32',
688
+ ia32: 'x86 (IA-32)',
689
+ mips: 'MIPS',
690
+ mipsel: 'MIPS (LE)',
691
+ ppc64: 'PowerPC 64',
692
+ s390x: 's390x',
693
+ riscv64: 'RISC-V 64',
694
+ };
695
+
696
+ /**
697
+ * Detect detailed platform information: environment type, OS, architecture, kernel.
698
+ * @param {boolean} verbose - Enable verbose logging
699
+ * @returns {Promise<{environment: string, arch: string, os: string, kernel: string}>}
700
+ */
701
+ async function detectPlatformInfo(verbose) {
702
+ const info = { environment: '', arch: '', os: '', kernel: '' };
703
+
704
+ // Architecture
705
+ info.arch = ARCH_NAMES[process.arch] || process.arch;
706
+
707
+ // Kernel
708
+ const uname = await execCommandAsync('uname -r 2>/dev/null');
709
+ if (uname) {
710
+ info.kernel = `Linux ${uname}`;
711
+ } else if (process.platform === 'darwin') {
712
+ const darwinVer = await execCommandAsync('uname -r 2>/dev/null');
713
+ info.kernel = darwinVer ? `Darwin ${darwinVer}` : 'Darwin';
714
+ } else if (process.platform === 'win32') {
715
+ info.kernel = 'Windows NT';
716
+ }
717
+
718
+ // OS detection
719
+ if (process.platform === 'linux') {
720
+ // Try /etc/os-release for distro info
721
+ const osRelease = await execCommandAsync('cat /etc/os-release 2>/dev/null');
722
+ if (osRelease) {
723
+ const nameMatch = osRelease.match(/^PRETTY_NAME="?([^"\n]+)"?/m);
724
+ if (nameMatch) {
725
+ info.os = nameMatch[1];
726
+ } else {
727
+ const idMatch = osRelease.match(/^ID="?([^"\n]+)"?/m);
728
+ const versionMatch = osRelease.match(/^VERSION_ID="?([^"\n]+)"?/m);
729
+ if (idMatch) {
730
+ info.os = versionMatch ? `${idMatch[1]} ${versionMatch[1]}` : idMatch[1];
731
+ }
732
+ }
733
+ }
734
+ if (!info.os) info.os = 'Linux';
735
+ } else if (process.platform === 'darwin') {
736
+ const swVers = await execCommandAsync('sw_vers -productVersion 2>/dev/null');
737
+ info.os = swVers ? `macOS ${swVers}` : 'macOS';
738
+ } else if (process.platform === 'win32') {
739
+ info.os = 'Windows';
740
+ } else {
741
+ info.os = process.platform;
742
+ }
743
+
744
+ // Environment detection: docker container, VM, or host
745
+ const isDocker = await execCommandAsync('cat /proc/1/cgroup 2>/dev/null | grep -qi docker && echo docker || test -f /.dockerenv && echo docker || echo no');
746
+ if (isDocker && isDocker.trim() === 'docker') {
747
+ info.environment = 'docker container';
748
+ } else {
749
+ // Check for VM/hypervisor
750
+ const systemdDetect = await execCommandAsync('systemd-detect-virt 2>/dev/null');
751
+ if (systemdDetect && systemdDetect !== 'none') {
752
+ info.environment = `virtual machine (${systemdDetect})`;
753
+ } else {
754
+ const dmi = await execCommandAsync('cat /sys/class/dmi/id/product_name 2>/dev/null');
755
+ if (dmi && /virtual|vmware|kvm|qemu|hyper-v|xen|bochs/i.test(dmi)) {
756
+ info.environment = `virtual machine`;
757
+ } else {
758
+ info.environment = 'host machine';
759
+ }
760
+ }
761
+ }
762
+
763
+ if (verbose) {
764
+ console.log(`[VERBOSE] Platform detection: ${JSON.stringify(info)}`);
765
+ }
766
+
767
+ return info;
768
+ }
769
+
562
770
  /**
563
771
  * Get comprehensive version information for all components
564
772
  * Uses Promise.all for parallel execution (issue #1320)
@@ -595,12 +803,16 @@ export async function getVersionInfo(verbose = false, processVersion = null) {
595
803
  console.log(`[VERBOSE] Node.js version: ${versions.node}`);
596
804
  }
597
805
 
598
- // Platform information
599
- const platform = process.platform;
600
- const arch = process.arch;
601
- versions.platform = `${platform} (${arch})`;
806
+ // Platform information — detailed detection
807
+ const platformInfo = await detectPlatformInfo(verbose);
808
+ versions.platformEnvironment = platformInfo.environment;
809
+ versions.platformArch = platformInfo.arch;
810
+ versions.platformOs = platformInfo.os;
811
+ versions.platformKernel = platformInfo.kernel;
812
+ // Keep legacy field for backward compat
813
+ versions.platform = platformInfo.os;
602
814
  if (verbose) {
603
- console.log(`[VERBOSE] Platform: ${versions.platform}`);
815
+ console.log(`[VERBOSE] Platform: env=${platformInfo.environment}, arch=${platformInfo.arch}, os=${platformInfo.os}, kernel=${platformInfo.kernel}`);
604
816
  }
605
817
 
606
818
  // Check if process version differs from installed version (restart warning)
@@ -717,8 +929,12 @@ export async function getVersionInfo(verbose = false, processVersion = null) {
717
929
  screen: versions.screen,
718
930
  xvfb: versions.xvfb,
719
931
 
720
- // Platform
932
+ // Platform (detailed)
721
933
  platform: versions.platform,
934
+ platformEnvironment: versions.platformEnvironment,
935
+ platformArch: versions.platformArch,
936
+ platformOs: versions.platformOs,
937
+ platformKernel: versions.platformKernel,
722
938
  },
723
939
  // Performance metrics
724
940
  gatherTimeMs: Date.now() - startTime,
@@ -941,7 +1157,7 @@ export function formatVersionMessage(versions) {
941
1157
 
942
1158
  if (cppLines.length > 0) {
943
1159
  lines.push('');
944
- lines.push('*🔨 C/C++/Assembly*');
1160
+ lines.push('*🔨 C, C++, Assembly*');
945
1161
  lines.push(...cppLines);
946
1162
  }
947
1163
 
@@ -997,11 +1213,22 @@ export function formatVersionMessage(versions) {
997
1213
  lines.push(...toolLines);
998
1214
  }
999
1215
 
1000
- // === Platform ===
1001
- if (versions.platform) {
1002
- lines.push('');
1003
- lines.push('*💻 Platform*');
1004
- lines.push(`• System: \`${versions.platform}\``);
1216
+ // === Platform (detailed) ===
1217
+ {
1218
+ const platformLines = [];
1219
+ if (versions.platformEnvironment) platformLines.push(`• Environment: \`${versions.platformEnvironment}\``);
1220
+ if (versions.platformArch) platformLines.push(`• Architecture: \`${versions.platformArch}\``);
1221
+ if (versions.platformOs) platformLines.push(`• OS: \`${versions.platformOs}\``);
1222
+ if (versions.platformKernel) platformLines.push(`• Kernel: \`${versions.platformKernel}\``);
1223
+ // Fallback to legacy single-line format
1224
+ if (platformLines.length === 0 && versions.platform) {
1225
+ platformLines.push(`• System: \`${versions.platform}\``);
1226
+ }
1227
+ if (platformLines.length > 0) {
1228
+ lines.push('');
1229
+ lines.push('*💻 Platform*');
1230
+ lines.push(...platformLines);
1231
+ }
1005
1232
  }
1006
1233
 
1007
1234
  return lines.join('\n');
@@ -1011,4 +1238,5 @@ export default {
1011
1238
  getVersionInfo,
1012
1239
  formatVersionMessage,
1013
1240
  parseVersion,
1241
+ normalizeDate,
1014
1242
  };