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

stacklok / toolhive-studio / 21517492961

30 Jan 2026 01:30PM UTC coverage: 55.297% (+2.0%) from 53.322%
21517492961

Pull #1513

github

web-flow
Merge dde9744f8 into 41647b5d0
Pull Request #1513: feat(cli): add CLI alignment flow

2410 of 4590 branches covered (52.51%)

Branch coverage included in aggregate %.

461 of 612 new or added lines in 18 files covered. (75.33%)

2 existing lines in 2 files now uncovered.

3843 of 6718 relevant lines covered (57.2%)

119.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 } from '@common/types/cli'
13
import type { Platform } from './types'
14
import log from '../logger'
15

16
const execFileAsync = promisify(execFile)
1✔
17

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

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

35
function findWingetCliPaths(): string[] {
36
  const localAppData =
NEW
37
    process.env.LOCALAPPDATA || path.join(homedir(), 'AppData', 'Local')
×
NEW
38
  const wingetPackagesDir = path.join(
×
39
    localAppData,
40
    'Microsoft',
41
    'WinGet',
42
    'Packages'
43
  )
44

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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