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
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ jobs:
echo "CC_aarch64_linux_android=$NDK_BIN/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "AR_aarch64_linux_android=$NDK_BIN/llvm-ar" >> "$GITHUB_ENV"
shell: "bash"
- uses: swatinem/rust-cache@v2
with:
key: ${{ join(matrix.targets, '-') }}
cache-provider: ${{ matrix.cache_provider }}
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
Expand Down
2 changes: 1 addition & 1 deletion 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 Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hcom"
version = "0.7.22"
version = "0.7.23"
edition = "2024"
authors = ["hcom"]
description = "Connect Claude Code, Gemini CLI, Codex, OpenCode, Kilo Code, Pi, Oh My Pi, Antigravity, Cursor, Kimi, and Copilot so agents can message, watch, and spawn each other across terminals"
Expand Down
3 changes: 3 additions & 0 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ members = ["cargo:."]
cargo-dist-version = "0.31.0"
# CI backends to support
ci = "github"
# Build and upload every release artifact on the release PR without publishing.
# Revert to the default "plan" mode after v0.7.23 ships.
pr-run-mode = "upload"
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-linux-android", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
# The installers to generate for each app
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "hcom"
version = "0.7.22"
version = "0.7.23"
description = "Let AI agents message, watch, and spawn each other across terminals. Claude Code, Gemini CLI, Codex, OpenCode, Kilo Code, Pi, Oh My Pi, Antigravity, Cursor, Kimi, Copilot."
readme = "README.md"
license = "MIT"
Expand Down
30 changes: 29 additions & 1 deletion src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,38 @@ pub fn atomic_write_io(filepath: &Path, content: &str) -> std::io::Result<()> {
tmp.as_file().sync_all()?;

// Persist atomically (temp file → target path via rename)
tmp.persist(filepath).map_err(|e| e.error)?;
persist_temp_file(tmp, filepath)?;
Ok(())
}

#[cfg(not(windows))]
fn persist_temp_file(tmp: tempfile::NamedTempFile, filepath: &Path) -> std::io::Result<()> {
tmp.persist(filepath).map(|_| ()).map_err(|e| e.error)
}

#[cfg(windows)]
fn persist_temp_file(mut tmp: tempfile::NamedTempFile, filepath: &Path) -> std::io::Result<()> {
// MoveFileExW can transiently return ERROR_ACCESS_DENIED while antivirus,
// indexing, or another reader briefly holds the destination. Preserve the
// same temp file and retry the atomic replacement for a short bounded
// window instead of failing a config update immediately.
const MAX_ATTEMPTS: u64 = 6;
for attempt in 1..=MAX_ATTEMPTS {
match tmp.persist(filepath) {
Ok(_) => return Ok(()),
Err(err)
if err.error.kind() == std::io::ErrorKind::PermissionDenied
&& attempt < MAX_ATTEMPTS =>
{
tmp = err.file;
std::thread::sleep(std::time::Duration::from_millis(10 * attempt));
}
Err(err) => return Err(err.error),
}
}
unreachable!("persist loop returns on success or final error")
}

/// Write content to file atomically (temp file + rename).
/// Returns true on success, false on failure.
pub fn atomic_write(filepath: &Path, content: &str) -> bool {
Expand Down
24 changes: 18 additions & 6 deletions tests/support/claude_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ impl ToolCase for ClaudeCase {
}

fn prepare(&self, h: &Hcom, base_url: &str) {
// Skip Claude's global first-run theme picker. On Windows, hcom's
// synchronous launch can remain inside that picker until its readiness
// timeout, after which the test no longer has an inject endpoint to
// drive it. Workspace trust is project-scoped and still starts fresh,
// so the tests continue to exercise the trust gate required for hooks.
std::fs::write(
h.claude_home.join(".claude.json"),
serde_json::to_vec(&json!({"hasCompletedOnboarding": true}))
.expect("serialize Claude onboarding state"),
)
.expect("seed Claude onboarding state");

// Provider routing + isolated config must survive hcom's CI=1 clean-shell
// launch rebuild, so they go through the `$HCOM_DIR/env` passthrough.
let claude_home = h
Expand Down Expand Up @@ -219,12 +231,12 @@ impl ToolCase for ClaudeCase {
}

fn drive_startup(&self, h: &Hcom, name: &str) {
// With onboarding NOT suppressed (no IS_DEMO), a fresh CLAUDE_CONFIG_DIR
// makes Claude surface its theme picker then its workspace-trust dialog,
// which hcom reports as `launch_blocked`. Accepting trust here is what
// lets Claude register hooks at all ("Skipping ... hook execution -
// workspace trust not accepted" otherwise). Drive each surfaced prompt's
// default (theme=dark, trust=Yes) until the empty input prompt appears.
// Global onboarding is pre-seeded in prepare(), but the fresh workspace
// still surfaces its trust dialog, which hcom reports as
// `launch_blocked`. Accepting trust here is what lets Claude register
// hooks at all ("Skipping ... hook execution - workspace trust not
// accepted" otherwise). Keep the theme handling as a compatibility
// fallback for Claude versions that ignore the seeded state.
let deadline = Instant::now() + Duration::from_secs(90);
let mut last_screen = String::new();
while Instant::now() < deadline {
Expand Down
Loading