forked from op7418/CodePilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli-tools-detect.ts
More file actions
141 lines (123 loc) · 4.54 KB
/
cli-tools-detect.ts
File metadata and controls
141 lines (123 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { execFile } from 'child_process';
import { promisify } from 'util';
import { isWindows, getExpandedPath } from './platform';
import { CLI_TOOLS_CATALOG, EXTRA_WELL_KNOWN_BINS } from './cli-tools-catalog';
import type { CliToolRuntimeInfo, CliToolDefinition } from '@/types';
const execFileAsync = promisify(execFile);
// Module-level cache with TTL
let _cache: { catalog: CliToolRuntimeInfo[]; extra: CliToolRuntimeInfo[]; timestamp: number } | null = null;
const CACHE_TTL = 120_000; // 2 minutes
/**
* Detect a single CLI tool — checks binNames with which/where, then runs --version
*/
export async function detectCliTool(tool: CliToolDefinition): Promise<CliToolRuntimeInfo> {
const expandedPath = getExpandedPath();
const env = { ...process.env, PATH: expandedPath };
for (const bin of tool.binNames) {
try {
// Find binary path
const whichCmd = isWindows ? 'where' : '/usr/bin/which';
const { stdout: binPath } = await execFileAsync(whichCmd, [bin], {
timeout: 5000,
env,
shell: isWindows,
});
const resolvedPath = binPath.trim().split(/\r?\n/)[0]?.trim();
if (!resolvedPath) continue;
// Get version
let version: string | null = null;
try {
const { stdout: versionOut, stderr: versionErr } = await execFileAsync(resolvedPath, ['--version'], {
timeout: 5000,
env,
});
const versionText = (versionOut || versionErr).trim();
// Extract version number from first line
const match = versionText.split('\n')[0]?.match(/(\d+\.\d+[\w.-]*)/);
version = match ? match[1] : versionText.split('\n')[0]?.slice(0, 50) || null;
} catch {
// Binary exists but --version failed — still mark as installed
}
return {
id: tool.id,
status: 'installed',
version,
binPath: resolvedPath,
};
} catch {
// bin not found, try next
}
}
return {
id: tool.id,
status: 'not_installed',
version: null,
binPath: null,
};
}
/**
* Detect a single binary by name (for extra well-known tools outside catalog)
*/
async function detectBinary(id: string, bin: string): Promise<CliToolRuntimeInfo> {
const expandedPath = getExpandedPath();
const env = { ...process.env, PATH: expandedPath };
try {
const whichCmd = isWindows ? 'where' : '/usr/bin/which';
const { stdout: binPath } = await execFileAsync(whichCmd, [bin], {
timeout: 3000,
env,
shell: isWindows,
});
const resolvedPath = binPath.trim().split(/\r?\n/)[0]?.trim();
if (!resolvedPath) {
return { id, status: 'not_installed', version: null, binPath: null };
}
let version: string | null = null;
try {
const { stdout: vOut, stderr: vErr } = await execFileAsync(resolvedPath, ['--version'], {
timeout: 3000,
env,
});
const vText = (vOut || vErr).trim();
const match = vText.split('\n')[0]?.match(/(\d+\.\d+[\w.-]*)/);
version = match ? match[1] : null;
} catch { /* version extraction optional */ }
return { id, status: 'installed', version, binPath: resolvedPath };
} catch {
return { id, status: 'not_installed', version: null, binPath: null };
}
}
export interface DetectAllResult {
catalog: CliToolRuntimeInfo[];
extra: CliToolRuntimeInfo[];
}
/**
* Detect all catalog tools + extra well-known binaries in parallel, with 2-minute cache.
* Returns separate arrays so the UI can distinguish catalog vs system-detected tools.
*/
export async function detectAllCliTools(forceRefresh = false): Promise<DetectAllResult> {
const now = Date.now();
if (!forceRefresh && _cache && now - _cache.timestamp < CACHE_TTL) {
return { catalog: _cache.catalog, extra: _cache.extra };
}
// Collect catalog tool IDs so we skip duplicates from the extra list
const catalogBins = new Set(CLI_TOOLS_CATALOG.flatMap(t => t.binNames));
const [catalogResults, extraResults] = await Promise.all([
Promise.all(CLI_TOOLS_CATALOG.map(tool => detectCliTool(tool))),
Promise.all(
EXTRA_WELL_KNOWN_BINS
.filter(([, , bin]) => !catalogBins.has(bin))
.map(([id, , bin]) => detectBinary(id, bin))
),
]);
// Only keep extra tools that are actually installed
const extraInstalled = extraResults.filter(t => t.status === 'installed');
_cache = { catalog: catalogResults, extra: extraInstalled, timestamp: now };
return { catalog: catalogResults, extra: extraInstalled };
}
/**
* Invalidate the detection cache
*/
export function invalidateDetectCache(): void {
_cache = null;
}