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

stacklok / toolhive-studio / 21480687144

29 Jan 2026 01:49PM UTC coverage: 53.56% (+0.2%) from 53.313%
21480687144

Pull #1513

github

web-flow
Merge 1fa1b78a5 into c83f585bd
Pull Request #1513: feat(cli): add CLI alignment flow

2348 of 4561 branches covered (51.48%)

287 of 527 new or added lines in 17 files covered. (54.46%)

2 existing lines in 2 files now uncovered.

3648 of 6634 relevant lines covered (54.99%)

121.03 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.X.X" 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
function findWingetCliPaths(): string[] {
35
  const localAppData =
NEW
36
    process.env.LOCALAPPDATA || path.join(homedir(), 'AppData', 'Local')
×
NEW
37
  const wingetPackagesDir = path.join(
×
38
    localAppData,
39
    'Microsoft',
40
    'WinGet',
41
    'Packages'
42
  )
43

NEW
44
  if (!existsSync(wingetPackagesDir)) {
×
NEW
45
    return []
×
46
  }
47

NEW
48
  try {
×
NEW
49
    const entries = readdirSync(wingetPackagesDir, { withFileTypes: true })
×
NEW
50
    const thvPaths: string[] = []
×
51

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

NEW
65
    return thvPaths
×
66
  } catch {
NEW
67
    return []
×
68
  }
69
}
70

71
export async function detectExternalCli(
72
  platform: Platform = process.platform as Platform
6✔
73
): Promise<ExternalCliInfo | null> {
74
  const pathsToCheck = [...(EXTERNAL_CLI_PATHS[platform] ?? [])]
6!
75

76
  // On Windows, also scan the WinGet packages directory
77
  if (platform === 'win32') {
6!
NEW
78
    pathsToCheck.push(...findWingetCliPaths())
×
79
  }
80

81
  const existingPath = pathsToCheck.find((p) => existsSync(p))
9✔
82

83
  if (!existingPath) {
6✔
84
    return null
1✔
85
  }
86

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

89
  const version = await getCliVersion(existingPath)
5✔
90

91
  return {
5✔
92
    path: existingPath,
93
    version,
94
    source: getCliSourceFromPath(existingPath, platform),
95
  }
96
}
97

98
export async function getCliInfo(cliPath: string): Promise<{
99
  exists: boolean
100
  version: string | null
101
  isExecutable: boolean
102
}> {
103
  const exists = existsSync(cliPath)
3✔
104

105
  if (!exists) {
3✔
106
    return { exists: false, version: null, isExecutable: false }
1✔
107
  }
108

109
  const version = await getCliVersion(cliPath)
2✔
110

111
  return {
2✔
112
    exists: true,
113
    version,
114
    isExecutable: version !== null,
115
  }
116
}
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