Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,35 @@ jobs:

- name: Rust tests
run: cargo test --workspace

test-windows:
# HarnessKit ships a Windows CLI, and the desktop app now builds on Windows,
# so run the same checks there: path separators, process spawning and home
# resolution all differ, and macOS-only CI cannot catch regressions in them.
# Pinned to windows-2025 like release.yml; never windows-latest.
runs-on: windows-2025
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2

- name: Install frontend dependencies
run: npm ci

- name: Frontend tests
run: npm test

- name: Build frontend
run: npm run build

- name: Rust tests
run: cargo test --workspace
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ cargo run -p hk-cli -- serve # http://127.0.0.1:7070

Open `http://localhost:1420` in your browser. Vite proxies `/api/*` requests to the backend at `:7070`.

### Desktop App Development (macOS only)
### Desktop App Development (macOS / Windows)

```bash
cargo tauri dev
```

Tauri automatically runs `npm run dev` as a before-dev command and launches the native window.
Tauri automatically runs `npm run dev` as a before-dev command and launches the native window. On Windows, the Visual Studio Build Tools ("Desktop development with C++") that rustup asks for are the only extra prerequisite. `cargo tauri build` packages NSIS and MSI installers under `target/release/bundle/`; because `createUpdaterArtifacts` is on, it also needs `TAURI_SIGNING_PRIVATE_KEY` in the environment, or pass `--config '{"bundle":{"createUpdaterArtifacts":false}}'` for a local build without updater artifacts.

## Building Releases

Expand Down Expand Up @@ -89,6 +89,8 @@ npm test # frontend tests (vitest)
cargo test --workspace # Rust tests
```

Tests that need an isolated home directory set `HARNESSKIT_HOME`, which `hk_core::home_dir()` honors ahead of the real home. It exists for test isolation (on Windows the home directory cannot be redirected through `HOME`); it is not a supported way to relocate HarnessKit's data.

## Troubleshooting

### `Found version mismatched Tauri packages`
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/hk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ fn main() -> Result<()> {
}

fn hk_data_dir() -> PathBuf {
dirs::home_dir().unwrap_or_default().join(".harnesskit")
hk_core::home_dir().join(".harnesskit")
}

/// Resolve the web access token for `hk serve`.
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl Default for AntigravityAdapter {
impl AntigravityAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl Default for ClaudeAdapter {
impl ClaudeAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl Default for CodexAdapter {
impl CodexAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/hk-core/src/adapter/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl Default for CopilotAdapter {
impl CopilotAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down Expand Up @@ -520,7 +520,7 @@ mod tests {
&installed,
format!(
r#"{{"version":1,"installed":[{{"pluginUri":"file://{}","marketplace":"user/my-repo"}}]}}"#,
plugin_dir.to_string_lossy()
plugin_dir.to_string_lossy().replace('\\', "/")
),
).unwrap();

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Default for CursorAdapter {
impl CursorAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/dsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ fn resolve_homes(

impl DshAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
let (dsh_home, agents_home) = resolve_homes(
std::env::var_os("DSH_HOME"),
std::env::var_os("DSH_AGENTS_HOME"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl Default for GeminiAdapter {
impl GeminiAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
4 changes: 2 additions & 2 deletions crates/hk-core/src/adapter/grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ impl Default for GrokAdapter {

impl GrokAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
grok_home: resolve_grok_home(std::env::var_os("GROK_HOME"), Some(&home))
.unwrap_or_else(|| home.join(".grok")),
Expand All @@ -481,7 +481,7 @@ impl GrokAdapter {
/// moves neither — so don't scan skills or evaluate plugin enablement
/// through an adapter built this way in tests.
pub fn with_grok_home(grok_home: PathBuf) -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
grok_home,
agents_home: home.join(".agents"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Default for HermesAdapter {
impl HermesAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/kiro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl Default for KiroAdapter {
impl KiroAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/omp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Default for OmpAdapter {
impl OmpAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
8 changes: 6 additions & 2 deletions crates/hk-core/src/adapter/openclaw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ fn user_path(home: &Path, raw: &str) -> Option<PathBuf> {
None => PathBuf::from(raw),
},
};
path.is_absolute().then_some(path)
// `is_absolute()` is false on Windows for POSIX-style absolute paths
// like `/srv/claw` (the gateway's own docs and default examples use
// that shape); treat any path with a root — including a drive-relative
// `\srv` — as resolved, so only truly relative values stay unset.
(path.is_absolute() || path.has_root()).then_some(path)
}

/// Agent ids are normalised before they name a directory
Expand All @@ -94,7 +98,7 @@ impl OpenClawAdapter {
pub fn new() -> Self {
let var = |name| std::env::var_os(name).filter(|v| !v.is_empty());
Self::from_env(
dirs::home_dir().unwrap_or_default(),
crate::home_dir(),
var("OPENCLAW_STATE_DIR"),
var("OPENCLAW_CONFIG_PATH"),
var("OPENCLAW_WORKSPACE_DIR"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl Default for OpencodeAdapter {
impl OpencodeAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/penguin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn subdirs(dir: &Path) -> impl Iterator<Item = PathBuf> + use<> {

impl PenguinAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
penguin_home: resolve_home(std::env::var_os("PENGUIN_HOME"), &home),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/pi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Default for PiAdapter {

impl PiAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".pi").join("agent"));
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/qoder_cn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Default for QoderCnAdapter {
impl QoderCnAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/windsurf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Default for WindsurfAdapter {
impl WindsurfAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
6 changes: 2 additions & 4 deletions crates/hk-core/src/auditor/rules/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ impl AuditRule for CliCredentialStorage {
};

if let Some(cred_path) = &meta.credentials_path {
let expanded = if cred_path.starts_with("~/") {
dirs::home_dir()
.map(|h| h.join(&cred_path[2..]).to_string_lossy().to_string())
.unwrap_or_else(|| cred_path.clone())
let expanded = if let Some(rest) = cred_path.strip_prefix("~/") {
crate::home_dir().join(rest).to_string_lossy().to_string()
} else {
cred_path.clone()
};
Expand Down
7 changes: 4 additions & 3 deletions crates/hk-core/src/kits/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use crate::HkError;
use std::path::PathBuf;

/// Canonical directory holding all Kit zip files: `~/.harnesskit/kits/`.
///
/// The home root is `crate::home_dir()`, which tests redirect through
/// `HARNESSKIT_HOME` (the only override that works on Windows).
pub fn kits_dir() -> Result<PathBuf, HkError> {
let home = dirs::home_dir()
.ok_or_else(|| HkError::Internal("home directory not found".into()))?;
Ok(home.join(".harnesskit").join("kits"))
Ok(crate::home_dir().join(".harnesskit").join("kits"))
}

/// Ensure `~/.harnesskit/kits/` exists; idempotent.
Expand Down
6 changes: 3 additions & 3 deletions crates/hk-core/src/kits/tests/end_to_end.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::adapter::all_adapters;
use crate::kits::service::{create_kit, export_kit, import_kit, sync_kit_to_project, unsync_kit_from_project};
use crate::kits::tests::service::RestoreHome;
use crate::kits::tests::service::{set_test_home, RestoreHome};
use crate::kits::types::{
CreateKitRequest, KitConfigFileRef, SyncKitRequest, UnsyncKitRequest,
};
Expand All @@ -20,7 +20,7 @@ fn open(dir: &std::path::Path) -> Mutex<Store> {
fn full_lifecycle_create_install_uninstall() {
let dir = tempdir().unwrap();
let _restore = RestoreHome::new();
unsafe { std::env::set_var("HOME", dir.path()); }
set_test_home(dir.path());
let store = open(dir.path());
let adapters = all_adapters();
let project = dir.path().join("proj");
Expand Down Expand Up @@ -74,7 +74,7 @@ fn full_lifecycle_create_install_uninstall() {
fn export_import_round_trip_preserves_manifest_bytes() {
let dir = tempdir().unwrap();
let _restore = RestoreHome::new();
unsafe { std::env::set_var("HOME", dir.path()); }
set_test_home(dir.path());
let store = open(dir.path());
let adapters = all_adapters();
let src = dir.path().join("CLAUDE.md");
Expand Down
6 changes: 5 additions & 1 deletion crates/hk-core/src/kits/tests/paths.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use crate::kits::paths::{kits_dir, zip_path_for};
use serial_test::serial;

#[test]
fn zip_path_for_uses_hk_kit_zip_suffix() {
let p = zip_path_for("abc-123").unwrap();
assert!(p.to_string_lossy().ends_with("abc-123.hk-kit.zip"));
}

/// Reads the home twice (`kits_dir` and the assertion); the kit tests
/// rewrite it, so take the same lock.
#[test]
#[serial(kit_env)]
fn kits_dir_is_under_home() {
let dir = kits_dir().unwrap();
let home = dirs::home_dir().unwrap();
let home = crate::home_dir();
assert!(dir.starts_with(home));
assert!(dir.ends_with(".harnesskit/kits") || dir.ends_with(r".harnesskit\kits"));
}
Loading
Loading