@link-assistant/hive-mind 1.46.1 → 1.46.3
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 +23 -0
- package/package.json +1 -1
- package/src/option-suggestions.lib.mjs +1 -0
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.repository.lib.mjs +74 -67
- package/src/version-info.lib.mjs +287 -59
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.46.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c425744: Standardize /version output — strip OS/arch, normalize dates, enhance platform detection (Issue #1524)
|
|
8
|
+
- Strip OS/architecture info (e.g. x86_64-unknown-linux-gnu, linux/amd64) from version strings for cleaner output
|
|
9
|
+
- Normalize date formats to ISO (YYYY-MM-DD) across all version components
|
|
10
|
+
- Enhance platform detection for consistent environment reporting
|
|
11
|
+
|
|
12
|
+
## 1.46.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 37daeb7: Auto-recover from non-fork repositories during fork validation (Issue #1518)
|
|
17
|
+
- When a repository exists but is NOT a proper GitHub fork (or has wrong parent), safely auto-recover by comparing commits against upstream first — only delete and re-fork if no additional commits would be lost
|
|
18
|
+
- Add verbose logging of fork commands for debugging non-fork creation scenarios
|
|
19
|
+
- Add post-creation fork validation to detect non-fork repos immediately after `gh repo fork`
|
|
20
|
+
- Report non-fork creation to Sentry for monitoring
|
|
21
|
+
- Add `--allow-force-non-fork-repository-deletion` flag to force deletion even when additional commits would be lost
|
|
22
|
+
- Add case study documenting the root cause analysis of konard/MixaByk1996-elements-app
|
|
23
|
+
- Document all previously undocumented solve options in CONFIGURATION.md (12 options including --allow-force-non-fork-repository-deletion)
|
|
24
|
+
- Add CI/CD test to verify documentation stays in sync with code options (prevents drift)
|
|
25
|
+
|
|
3
26
|
## 1.46.1
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -199,6 +199,7 @@ const KNOWN_OPTION_NAMES = [
|
|
|
199
199
|
'only-prepare-command',
|
|
200
200
|
'auto-merge-default-branch-to-pull-request-branch',
|
|
201
201
|
'allow-fork-divergence-resolution-using-force-push-with-lease',
|
|
202
|
+
'allow-force-non-fork-repository-deletion',
|
|
202
203
|
'allow-to-push-to-contributors-pull-requests-as-maintainer',
|
|
203
204
|
'prefix-fork-name-with-owner-name',
|
|
204
205
|
'auto-restart-max-iterations',
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -273,6 +273,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
273
273
|
description: 'Allow automatic force-push (--force-with-lease) when fork diverges from upstream (DANGEROUS: can overwrite fork history)',
|
|
274
274
|
default: false,
|
|
275
275
|
},
|
|
276
|
+
'allow-force-non-fork-repository-deletion': {
|
|
277
|
+
type: 'boolean',
|
|
278
|
+
description: 'Allow deletion of non-fork repositories even when they contain additional commits that would be lost (DANGEROUS: data loss possible)',
|
|
279
|
+
default: false,
|
|
280
|
+
},
|
|
276
281
|
'allow-to-push-to-contributors-pull-requests-as-maintainer': {
|
|
277
282
|
type: 'boolean',
|
|
278
283
|
description: 'When continuing a fork PR as a maintainer, attempt to push directly to the contributor\'s fork if "Allow edits by maintainers" is enabled. Requires --auto-fork to be enabled.',
|
|
@@ -490,78 +490,77 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
490
490
|
|
|
491
491
|
const forkValidation = await validateForkParent(existingForkName, `${owner}/${repo}`);
|
|
492
492
|
|
|
493
|
-
if (
|
|
493
|
+
if (forkValidation.isValid) {
|
|
494
|
+
// Fork is valid — use it
|
|
495
|
+
await log(`${formatAligned('✅', 'Fork parent validated:', `${forkValidation.parent}`)}`);
|
|
496
|
+
repoToClone = existingForkName;
|
|
497
|
+
forkedRepo = existingForkName;
|
|
498
|
+
upstreamRemote = `${owner}/${repo}`;
|
|
499
|
+
} else if (forkValidation.isNetworkError) {
|
|
494
500
|
// Issue #1311: Handle network errors separately from fork mismatch errors
|
|
495
|
-
if (forkValidation.isNetworkError) {
|
|
496
|
-
await log('');
|
|
497
|
-
await log(`${formatAligned('❌', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
|
|
498
|
-
await log('');
|
|
499
|
-
await log(' 🔍 What happened:');
|
|
500
|
-
await log(` Failed to connect to GitHub API while validating fork.`);
|
|
501
|
-
await log(` Error: ${forkValidation.error}`);
|
|
502
|
-
await log('');
|
|
503
|
-
await log(' 💡 This is likely a temporary network issue. You can:');
|
|
504
|
-
await log(' 1. Wait a moment and try again');
|
|
505
|
-
await log(' 2. Check your internet connection');
|
|
506
|
-
await log(' 3. Check GitHub status: https://www.githubstatus.com/');
|
|
507
|
-
await log('');
|
|
508
|
-
await log(' Or use --no-fork to skip fork validation if you have write access.');
|
|
509
|
-
await log('');
|
|
510
|
-
await safeExit(1, 'Network error during fork validation - please retry');
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Fork parent mismatch detected - this prevents issue #967
|
|
514
501
|
await log('');
|
|
515
|
-
await log(`${formatAligned('❌', '
|
|
502
|
+
await log(`${formatAligned('❌', 'NETWORK ERROR DURING FORK VALIDATION', '')}`, { level: 'error' });
|
|
516
503
|
await log('');
|
|
517
504
|
await log(' 🔍 What happened:');
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
await log(' It may have been created by cloning and pushing instead of forking.');
|
|
521
|
-
} else {
|
|
522
|
-
await log(` Your fork ${existingForkName} was created from an intermediate fork,`);
|
|
523
|
-
await log(` not directly from the target repository ${owner}/${repo}.`);
|
|
524
|
-
}
|
|
525
|
-
await log('');
|
|
526
|
-
await log(' 📦 Fork relationship:');
|
|
527
|
-
await log(` • Your fork: ${existingForkName}`);
|
|
528
|
-
await log(` • Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}`);
|
|
529
|
-
await log(` • Fork source (root): ${forkValidation.source || 'N/A'}`);
|
|
530
|
-
await log(` • Expected parent: ${owner}/${repo}`);
|
|
531
|
-
await log('');
|
|
532
|
-
await log(' ⚠️ Why this is a problem:');
|
|
533
|
-
await log(' When a fork is created from an intermediate fork (a "fork of a fork"),');
|
|
534
|
-
await log(' any commits that exist in the intermediate fork but not in the target');
|
|
535
|
-
await log(' repository will be included in your pull requests. This can result in');
|
|
536
|
-
await log(' pull requests with hundreds or thousands of unexpected commits.');
|
|
505
|
+
await log(` Failed to connect to GitHub API while validating fork.`);
|
|
506
|
+
await log(` Error: ${forkValidation.error}`);
|
|
537
507
|
await log('');
|
|
538
|
-
await log('
|
|
539
|
-
await log('
|
|
540
|
-
await log('
|
|
541
|
-
await log('
|
|
508
|
+
await log(' 💡 This is likely a temporary network issue. You can:');
|
|
509
|
+
await log(' 1. Wait a moment and try again');
|
|
510
|
+
await log(' 2. Check your internet connection');
|
|
511
|
+
await log(' 3. Check GitHub status: https://www.githubstatus.com/');
|
|
542
512
|
await log('');
|
|
543
|
-
await log('
|
|
513
|
+
await log(' Or use --no-fork to skip fork validation if you have write access.');
|
|
544
514
|
await log('');
|
|
545
|
-
await
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
await log('');
|
|
549
|
-
await log(' Option 2: Use --prefix-fork-name-with-owner-name to create a new fork');
|
|
550
|
-
await log(` This creates a fork named ${currentUser}/${owner}-${repo} instead`);
|
|
551
|
-
await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --prefix-fork-name-with-owner-name --fork`);
|
|
552
|
-
await log('');
|
|
553
|
-
await log(' Option 3: Work directly on the repository (if you have write access)');
|
|
554
|
-
await log(` ./solve.mjs "${issueUrl || `https://github.com/${owner}/${repo}/issues/<number>`}" --no-fork`);
|
|
515
|
+
await safeExit(1, 'Network error during fork validation - please retry');
|
|
516
|
+
} else {
|
|
517
|
+
// Issue #1518: Auto-recovery — delete non-fork/mismatched repo and re-fork, but only if no commits would be lost
|
|
555
518
|
await log('');
|
|
556
|
-
|
|
557
|
-
|
|
519
|
+
await log(`${formatAligned('⚠️', 'FORK PARENT MISMATCH DETECTED', '')}`, { level: 'warning' });
|
|
520
|
+
const detail = !forkValidation.isFork ? `Repository ${existingForkName} is NOT a GitHub fork (see issue #1518)` : `Fork ${existingForkName} was created from ${forkValidation.parent} instead of ${owner}/${repo} (see issue #967)`;
|
|
521
|
+
await log(`${formatAligned('', '', detail)}`);
|
|
522
|
+
await log(`${formatAligned('', '', `Fork parent: ${forkValidation.parent || 'N/A (not a fork)'}, source: ${forkValidation.source || 'N/A'}, expected: ${owner}/${repo}`)}`);
|
|
523
|
+
// Safety check: compare commits before deleting to avoid data loss
|
|
524
|
+
await log(`${formatAligned('🔍', 'Safety check:', 'Comparing commits against upstream...')}`);
|
|
525
|
+
let safeToDelete = false;
|
|
526
|
+
try {
|
|
527
|
+
const cmp = await $`gh api repos/${owner}/${repo}/compare/${owner}:HEAD...${existingForkName.split('/')[0]}:HEAD --jq '.ahead_by' 2>&1`;
|
|
528
|
+
if (cmp.code === 0 && parseInt(cmp.stdout.toString().trim(), 10) === 0) {
|
|
529
|
+
await log(`${formatAligned('✅', 'Safe to delete:', 'No additional commits in non-fork repository')}`);
|
|
530
|
+
safeToDelete = true;
|
|
531
|
+
} else if (cmp.code === 0) {
|
|
532
|
+
await log(`${formatAligned('⚠️', 'UNSAFE:', `Repository has ${cmp.stdout.toString().trim()} commit(s) ahead of upstream that would be lost`)}`, { level: 'warning' });
|
|
533
|
+
} else {
|
|
534
|
+
await log(`${formatAligned('⚠️', 'Compare failed:', ((cmp.stderr?.toString() || '') + (cmp.stdout?.toString() || '')).split('\n')[0])}`, { level: 'warning' });
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
await log(`${formatAligned('⚠️', 'Compare error:', e.message)}`, { level: 'warning' });
|
|
538
|
+
}
|
|
539
|
+
if (!safeToDelete) {
|
|
540
|
+
if (argv.allowForceNonForkRepositoryDeletion) {
|
|
541
|
+
await log(`${formatAligned('⚠️', 'Force deletion ENABLED:', '--allow-force-non-fork-repository-deletion — proceeding despite potential data loss')}`, { level: 'warning' });
|
|
542
|
+
safeToDelete = true;
|
|
543
|
+
} else {
|
|
544
|
+
await log(` 💡 Manual fix required: back up work, then: gh repo delete ${existingForkName} --yes`);
|
|
545
|
+
await log(` Then run this command again to create a proper fork of ${owner}/${repo}`);
|
|
546
|
+
await log(` 🔧 Or force deletion (DANGEROUS): solve ${argv.url || argv['issue-url'] || argv._[0] || '<issue-url>'} --allow-force-non-fork-repository-deletion`);
|
|
547
|
+
await safeExit(1, 'Auto-recovery skipped - repository may contain commits that would be lost');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
await log(`${formatAligned('🔄', 'Auto-recovery:', 'Deleting non-fork repository and creating fresh fork...')}`);
|
|
551
|
+
const deleteResult = await $`gh repo delete ${existingForkName} --yes 2>&1`;
|
|
552
|
+
if (deleteResult.code !== 0) {
|
|
553
|
+
const delOut = (deleteResult.stderr?.toString() || '') + (deleteResult.stdout?.toString() || '');
|
|
554
|
+
await log(`${formatAligned('❌', 'Delete failed:', delOut.split('\n')[0])}`, { level: 'error' });
|
|
555
|
+
await log(` 💡 Manual fix: gh repo delete ${existingForkName} --yes, then re-run`);
|
|
556
|
+
await safeExit(1, 'Auto-recovery failed - could not delete problematic repository');
|
|
557
|
+
}
|
|
558
|
+
await log(`${formatAligned('✅', 'Deleted:', existingForkName)}`);
|
|
559
|
+
existingForkName = null; // Fall through to fork creation below
|
|
558
560
|
}
|
|
561
|
+
}
|
|
559
562
|
|
|
560
|
-
|
|
561
|
-
repoToClone = existingForkName;
|
|
562
|
-
forkedRepo = existingForkName;
|
|
563
|
-
upstreamRemote = `${owner}/${repo}`;
|
|
564
|
-
} else {
|
|
563
|
+
if (!existingForkName) {
|
|
565
564
|
// Need to create fork with retry logic for concurrent scenarios
|
|
566
565
|
await log(`${formatAligned('🔄', 'Creating fork...', '')}`);
|
|
567
566
|
|
|
@@ -575,19 +574,18 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
575
574
|
let actualForkName = `${currentUser}/${defaultForkName}`;
|
|
576
575
|
|
|
577
576
|
for (let attempt = 1; attempt <= maxForkRetries; attempt++) {
|
|
578
|
-
// Try to create fork with optional custom name
|
|
579
577
|
let forkResult;
|
|
578
|
+
// Issue #1518: Log the exact fork command for debugging non-fork creation scenarios
|
|
579
|
+
if (argv.verbose) await log(`${formatAligned('🔧', 'Fork command:', argv.prefixForkNameWithOwnerName ? `gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false` : `gh repo fork ${owner}/${repo} --clone=false`)}`);
|
|
580
580
|
if (argv.prefixForkNameWithOwnerName) {
|
|
581
|
-
// Use --fork-name flag to create fork with owner prefix
|
|
582
581
|
forkResult = await $`gh repo fork ${owner}/${repo} --fork-name ${owner}-${repo} --clone=false 2>&1`;
|
|
583
582
|
} else {
|
|
584
|
-
// Standard fork creation (no custom name)
|
|
585
583
|
forkResult = await $`gh repo fork ${owner}/${repo} --clone=false 2>&1`;
|
|
586
584
|
}
|
|
587
585
|
|
|
588
586
|
// Always capture output to parse actual fork name
|
|
589
587
|
const forkOutput = (forkResult.stderr ? forkResult.stderr.toString() : '') + (forkResult.stdout ? forkResult.stdout.toString() : '');
|
|
590
|
-
|
|
588
|
+
if (argv.verbose) await log(`${formatAligned('🔧', 'Fork output:', forkOutput.split('\n')[0] || '(empty)')}`); // Issue #1518
|
|
591
589
|
// Parse actual fork name from output (e.g., "konard/netkeep80-jsonRVM already exists")
|
|
592
590
|
// GitHub may create forks with modified names to avoid conflicts
|
|
593
591
|
// Use regex that won't match domain names like "github.com/user" -> "com/user"
|
|
@@ -792,11 +790,20 @@ Thank you!`;
|
|
|
792
790
|
await safeExit(1, 'Repository setup failed');
|
|
793
791
|
}
|
|
794
792
|
|
|
795
|
-
// Wait a moment for fork to be fully ready
|
|
796
793
|
if (forkCreated) {
|
|
794
|
+
// Wait a moment for fork to be fully ready
|
|
797
795
|
await log(`${formatAligned('⏳', 'Waiting:', 'For fork to be fully ready...')}`);
|
|
798
796
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
799
797
|
}
|
|
798
|
+
// Issue #1518: Validate fork parent after creation/discovery to detect non-fork repos early (covers concurrent worker scenarios too)
|
|
799
|
+
await log(`${formatAligned('🔍', 'Validating fork parent...', '')}`);
|
|
800
|
+
const pcv = await validateForkParent(actualForkName, `${owner}/${repo}`);
|
|
801
|
+
if (pcv.isValid) {
|
|
802
|
+
await log(`${formatAligned('✅', 'Fork parent validated:', `${pcv.parent}`)}`);
|
|
803
|
+
} else if (!pcv.isNetworkError) {
|
|
804
|
+
await log(`${formatAligned('⚠️', 'WARNING:', `Fork failed validation (possible gh CLI bug, see issue #1518): ${pcv.error}`)}`, { level: 'warning' });
|
|
805
|
+
reportError(new Error(`Fork created as non-fork: ${pcv.error}`), { context: 'fork_creation_validation', forkRepo: actualForkName, expectedUpstream: `${owner}/${repo}`, isFork: pcv.isFork, parent: pcv.parent, source: pcv.source });
|
|
806
|
+
}
|
|
800
807
|
}
|
|
801
808
|
|
|
802
809
|
repoToClone = actualForkName;
|
package/src/version-info.lib.mjs
CHANGED
|
@@ -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*)?)
|
|
146
|
+
const m = raw.match(/go([\d.]+(?:\S*)?)/);
|
|
64
147
|
if (!m) return null;
|
|
65
|
-
return { version: m[1], extra: [
|
|
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)
|
|
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
|
|
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+(?:\(
|
|
178
|
+
const m = raw.match(/^gcc\s+(?:\([^)]*\)\s+)?([\d.]+)/);
|
|
85
179
|
if (!m) return null;
|
|
86
|
-
|
|
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
|
|
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+(?:\(
|
|
184
|
+
const m = raw.match(/^g\+\+\s+(?:\([^)]*\)\s+)?([\d.]+)/);
|
|
93
185
|
if (!m) return null;
|
|
94
|
-
|
|
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
|
-
|
|
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])
|
|
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)
|
|
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
|
|
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+)?)
|
|
246
|
+
const m = raw.match(/^Swift\s+version\s+([\d.]+(?:\.\d+)?)/);
|
|
135
247
|
if (!m) return null;
|
|
136
|
-
return { version: m[1], extra:
|
|
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.]+)
|
|
279
|
+
const m = raw.match(/^curl\s+([\d.]+)/);
|
|
168
280
|
if (!m) return null;
|
|
169
|
-
return { version: m[1], extra:
|
|
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*(?:\(
|
|
315
|
+
const m = raw.match(/^Screen\s+version\s+([\d.]+)\s*(?:\([^)]*\))?\s*(.*)/);
|
|
204
316
|
if (!m) return null;
|
|
205
317
|
const extra = [];
|
|
206
|
-
|
|
207
|
-
if (
|
|
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(
|
|
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(
|
|
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.]+)
|
|
427
|
+
const m = raw.match(/([\d.]+)/);
|
|
310
428
|
if (!m) return null;
|
|
311
|
-
return { version: m[1], extra:
|
|
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
|
-
|
|
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
|
|
600
|
-
|
|
601
|
-
versions.
|
|
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:
|
|
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
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
};
|