• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / toolhive-studio / 21369282030

26 Jan 2026 06:31PM UTC coverage: 51.87% (-1.5%) from 53.322%
21369282030

Pull #1513

github

web-flow
Merge d27e5ef09 into 0c7b4231b
Pull Request #1513: feat(cli): add CLI alignment flow

2249 of 4547 branches covered (49.46%)

Branch coverage included in aggregate %.

195 of 522 new or added lines in 12 files covered. (37.36%)

2 existing lines in 2 files now uncovered.

3548 of 6629 relevant lines covered (53.52%)

120.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.84
/main/src/cli/cli-detection.ts
1
/**
2
 * CLI Detection
3
 * Detects externally installed CLI binaries (THV-0020)
4
 */
5

6
import { existsSync, readdirSync } from 'node:fs'
1✔
7
import { execFile } from 'node:child_process'
1✔
8
import { promisify } from 'node:util'
1✔
9
import path from 'node:path'
10
import { homedir } from 'node:os'
1✔
11
import { EXTERNAL_CLI_PATHS, getCliSourceFromPath } from './constants'
12
import type { ExternalCliInfo, Platform } from './types'
13
import log from '../logger'
14

15
const execFileAsync = promisify(execFile)
1✔
16

17
/** Extracts version from "ToolHive v0.7.2" line in `thv version` output */
18
const parseVersionOutput = (stdout: string): string | null => {
1✔
19
  const match = stdout.match(/^ToolHive v(\d+\.\d+\.\d+(?:-[\w.]+)?)/m)
5✔
20
  return match?.[1] ?? null
5!
21
}
22

23
const getCliVersion = async (cliPath: string): Promise<string | null> => {
1✔
24
  try {
7✔
25
    const { stdout } = await execFileAsync(cliPath, ['version'], {
7✔
26
      timeout: 5000,
27
    })
28
    return parseVersionOutput(stdout)
5✔
29
  } catch {
30
    return null
2✔
31
  }
32
}
33

34
/**
35
 * Scans the WinGet packages directory for ToolHive installations.
36
 * WinGet uses dynamic folder names like: stacklok.thv_Microsoft.Winget.Source_8wekyb3d8bbwe
37
 */
38
function findWingetCliPaths(): string[] {
39
  const localAppData =
NEW
40
    process.env.LOCALAPPDATA || path.join(homedir(), 'AppData', 'Local')
×
NEW
41
  const wingetPackagesDir = path.join(
×
42
    localAppData,
43
    'Microsoft',
44
    'WinGet',
45
    'Packages'
46
  )
47

NEW
48
  if (!existsSync(wingetPackagesDir)) {
×
NEW
49
    return []
×
50
  }
51

NEW
52
  try {
×
NEW
53
    const entries = readdirSync(wingetPackagesDir, { withFileTypes: true })
×
NEW
54
    const thvPaths: string[] = []
×
55

NEW
56
    for (const entry of entries) {
×
57
      // Match folders starting with 'stacklok.thv_' (case-insensitive)
NEW
58
      if (
×
59
        entry.isDirectory() &&
×
60
        entry.name.toLowerCase().startsWith('stacklok.thv_')
61
      ) {
NEW
62
        const cliPath = path.join(wingetPackagesDir, entry.name, 'thv.exe')
×
NEW
63
        if (existsSync(cliPath)) {
×
NEW
64
          thvPaths.push(cliPath)
×
65
        }
66
      }
67
    }
68

NEW
69
    return thvPaths
×
70
  } catch {
NEW
71
    return []
×
72
  }
73
}
74

75
export async function detectExternalCli(
76
  platform: Platform = process.platform as Platform
6✔
77
): Promise<ExternalCliInfo | null> {
78
  const pathsToCheck = [...(EXTERNAL_CLI_PATHS[platform] ?? [])]
6!
79

80
  // On Windows, also scan the WinGet packages directory
81
  if (platform === 'win32') {
6!
NEW
82
    pathsToCheck.push(...findWingetCliPaths())
×
83
  }
84

85
  const existingPath = pathsToCheck.find((p) => existsSync(p))
8✔
86

87
  if (!existingPath) {
6✔
88
    return null
1✔
89
  }
90

91
  log.info(`Found external CLI at: ${existingPath}`)
5✔
92

93
  const version = await getCliVersion(existingPath)
5✔
94

95
  return {
5✔
96
    path: existingPath,
97
    version,
98
    source: getCliSourceFromPath(existingPath, platform),
99
  }
100
}
101

102
export async function getCliInfo(cliPath: string): Promise<{
103
  exists: boolean
104
  version: string | null
105
  isExecutable: boolean
106
}> {
107
  const exists = existsSync(cliPath)
3✔
108

109
  if (!exists) {
3✔
110
    return { exists: false, version: null, isExecutable: false }
1✔
111
  }
112

113
  const version = await getCliVersion(cliPath)
2✔
114

115
  return {
2✔
116
    exists: true,
117
    version,
118
    isExecutable: version !== null,
119
  }
120
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc