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

stacklok / toolhive-studio / 21593381477

02 Feb 2026 02:10PM UTC coverage: 55.487% (+2.0%) from 53.49%
21593381477

push

github

web-flow
feat(cli): add CLI alignment flow (#1513)

* feat(cli): add core CLI alignment module

Add CLI module for desktop-owned CLI installation management (THV-0020):
- Types and constants for CLI paths and marker file schema
- Marker file management for tracking CLI installation source
- Symlink manager for creating/validating CLI symlinks (copy on Windows)
- PATH configurator for shell profile integration
- CLI detection to identify external CLI installations
- Validation logic for every-launch alignment checks
- Dialog helpers for user prompts on alignment issues

* test(cli): add CLI module unit tests

Add comprehensive tests for CLI alignment module:
- CLI detection tests
- Constants tests
- Marker file tests
- Symlink manager tests
- Validation tests

* feat(main): integrate CLI alignment validation on startup

Add CLI validation during app initialization (THV-0020):
- Validate CLI alignment before other setup tasks
- Skip validation in dev mode (bundled CLI may not exist)
- Add IPC handlers for CLI status, reinstall, and remove operations

* feat(preload): expose CLI alignment API to renderer

Add cliAlignment API to electronAPI bridge (THV-0020):
- getStatus: retrieve CLI installation status
- reinstall: reinstall CLI symlink
- remove: remove CLI installation
- getPathStatus: check shell PATH configuration

* feat(settings): add CLI management tab

Add new CLI tab to settings for managing CLI installation (THV-0020):
- Display CLI status, version, and install method
- Show CLI path and symlink target
- Display PATH configuration status
- Actions to verify, reinstall, or remove CLI
- Usage examples for common thv commands

* revert: remove some extra loggin

* test: adjust after latest changes

* refactor(cli): move CLI issue dialogs from main to renderer

Replace native Electron dialogs with a dedicated /cli-issue page in
the renderer for better UX and consistency.

- Remove main/src/cli/dialogs.ts (native Electron dialogs)
- Add renderer/src/routes/... (continued)

2444 of 4633 branches covered (52.75%)

Branch coverage included in aggregate %.

470 of 619 new or added lines in 18 files covered. (75.93%)

2 existing lines in 2 files now uncovered.

3886 of 6775 relevant lines covered (57.36%)

117.49 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