Skip to content

Commit 779a95f

Browse files
Merge pull request #9 from jhonatanjunio/main
feat: add Windows (Git Bash) support
2 parents ba04c51 + e06e747 commit 779a95f

11 files changed

Lines changed: 122 additions & 40 deletions

File tree

.github/workflows/release.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,23 @@ jobs:
6262
name: squeez-linux-aarch64
6363
path: squeez-linux-aarch64
6464

65+
build-windows-x86_64:
66+
runs-on: windows-latest
67+
steps:
68+
- uses: actions/checkout@v4
69+
- uses: dtolnay/rust-toolchain@stable
70+
- name: Build
71+
run: cargo build --release --target x86_64-pc-windows-msvc
72+
- name: Rename binary
73+
run: mv target/x86_64-pc-windows-msvc/release/squeez.exe squeez-windows-x86_64.exe
74+
- name: Upload artifact
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: squeez-windows-x86_64
78+
path: squeez-windows-x86_64.exe
79+
6580
release:
66-
needs: [build-macos, build-linux-x86_64, build-linux-aarch64]
81+
needs: [build-macos, build-linux-x86_64, build-linux-aarch64, build-windows-x86_64]
6782
runs-on: ubuntu-latest
6883
permissions:
6984
contents: write
@@ -75,11 +90,13 @@ jobs:
7590
cd squeez-macos-universal && sha256sum squeez-macos-universal > ../checksums.sha256 && cd ..
7691
cd squeez-linux-x86_64 && sha256sum squeez-linux-x86_64 >> ../checksums.sha256 && cd ..
7792
cd squeez-linux-aarch64 && sha256sum squeez-linux-aarch64 >> ../checksums.sha256 && cd ..
93+
cd squeez-windows-x86_64 && sha256sum squeez-windows-x86_64.exe >> ../checksums.sha256 && cd ..
7894
- name: Upload to release
7995
uses: softprops/action-gh-release@v1
8096
with:
8197
files: |
8298
squeez-macos-universal/squeez-macos-universal
8399
squeez-linux-x86_64/squeez-linux-x86_64
84100
squeez-linux-aarch64/squeez-linux-aarch64
101+
squeez-windows-x86_64/squeez-windows-x86_64.exe
85102
checksums.sha256

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
/bin/
33
*.DS_Store
44
.omc/
5+
docs/
6+
.worktrees/

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ name = "squeez"
1212
path = "src/lib.rs"
1313

1414
[dependencies]
15+
16+
[target.'cfg(unix)'.dependencies]
1517
libc = "0.2"
1618

1719
[profile.release]

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Token compression + context optimization for Claude Code, OpenCode, and GitHub C
1515

1616
## Install
1717

18+
> **Windows users:** squeez requires **Git Bash** to run. PowerShell and CMD are not supported — the hooks and binary rely on a POSIX shell environment. Open Git Bash and run:
19+
1820
```bash
1921
curl -fsSL https://raw.githubusercontent.com/claudioemmanuel/squeez/main/install.sh | sh
2022
```
@@ -130,7 +132,18 @@ SQUEEZ_DIR=~/.copilot/squeez ~/.claude/squeez/bin/squeez init --copilot
130132

131133
## Local development
132134

133-
**Prerequisites:** Rust stable (`rustup update stable`), `bash`, macOS or Linux.
135+
**Prerequisites:** Rust stable, `bash` (Git Bash on Windows — PowerShell is not supported). Works on Windows (Git Bash), macOS, and Linux.
136+
137+
Install Rust via [rustup](https://rust-lang.org/tools/install/):
138+
```bash
139+
# macOS / Linux
140+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
141+
142+
# Windows: download and run rustup-init.exe from https://rust-lang.org/tools/install/
143+
# Then restart your terminal so cargo is in PATH.
144+
# Windows users also need MSVC C++ Build Tools:
145+
# https://visualstudio.microsoft.com/visual-cpp-build-tools/
146+
```
134147

135148
```bash
136149
# 1. Clone

install.sh

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,49 @@ case "$OS" in
1717
*) echo "ERROR: unsupported arch $ARCH" >&2; exit 1 ;;
1818
esac
1919
;;
20-
Windows*|MINGW*|CYGWIN*)
21-
echo "ERROR: Windows não é suportado. Use macOS ou Linux." >&2
22-
exit 1
20+
Windows*|MINGW*|MSYS*|CYGWIN*)
21+
BINARY="squeez-windows-x86_64.exe"
2322
;;
2423
*) echo "ERROR: unsupported OS $OS" >&2; exit 1 ;;
2524
esac
2625

26+
# Local binary name: squeez.exe on Windows, squeez elsewhere
27+
case "$OS" in
28+
Windows*|MINGW*|MSYS*|CYGWIN*) BIN_NAME="squeez.exe" ;;
29+
*) BIN_NAME="squeez" ;;
30+
esac
31+
2732
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/hooks" "$INSTALL_DIR/sessions" "$INSTALL_DIR/memory"
28-
chmod 700 "$INSTALL_DIR" "$INSTALL_DIR/sessions" "$INSTALL_DIR/memory"
33+
chmod 700 "$INSTALL_DIR" "$INSTALL_DIR/sessions" "$INSTALL_DIR/memory" 2>/dev/null || true
2934

3035
echo "Downloading squeez binary for $OS/$ARCH..."
31-
curl -fsSL "$RELEASES/$BINARY" -o "$INSTALL_DIR/bin/squeez"
36+
curl -fsSL "$RELEASES/$BINARY" -o "$INSTALL_DIR/bin/$BIN_NAME"
3237

3338
echo "Verifying checksum..."
3439
curl -fsSL "$RELEASES/checksums.sha256" -o /tmp/squeez-checksums.sha256
3540
expected=$(grep "$BINARY" /tmp/squeez-checksums.sha256 2>/dev/null | awk '{print $1}')
3641
rm -f /tmp/squeez-checksums.sha256
3742
if [ -z "$expected" ]; then
3843
echo "ERROR: could not find checksum for $BINARY in release" >&2
39-
rm -f "$INSTALL_DIR/bin/squeez"
44+
rm -f "$INSTALL_DIR/bin/$BIN_NAME"
4045
exit 1
4146
fi
4247

43-
# Use sha256sum if available (Linux), otherwise fall back to shasum (macOS)
48+
# Use sha256sum if available (Linux/Windows Git Bash), otherwise fall back to shasum (macOS)
4449
if command -v sha256sum >/dev/null 2>&1; then
45-
actual=$(sha256sum "$INSTALL_DIR/bin/squeez" | awk '{print $1}')
50+
actual=$(sha256sum "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
4651
else
47-
actual=$(shasum -a 256 "$INSTALL_DIR/bin/squeez" | awk '{print $1}')
52+
actual=$(shasum -a 256 "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
4853
fi
4954

5055
if [ "$expected" != "$actual" ]; then
5156
echo "ERROR: checksum mismatch — binary may be corrupted or tampered" >&2
52-
rm -f "$INSTALL_DIR/bin/squeez"
57+
rm -f "$INSTALL_DIR/bin/$BIN_NAME"
5358
exit 1
5459
fi
5560
echo "Checksum verified."
5661

57-
chmod +x "$INSTALL_DIR/bin/squeez"
62+
chmod +x "$INSTALL_DIR/bin/$BIN_NAME" 2>/dev/null || true
5863

5964
echo "Installing hooks..."
6065
curl -fsSL "$REPO_RAW/hooks/pretooluse.sh" -o "$INSTALL_DIR/hooks/pretooluse.sh"
@@ -132,8 +137,8 @@ mkdir -p "$COPILOT_SQUEEZ_DIR/bin" "$COPILOT_SQUEEZ_DIR/hooks" \
132137
chmod 700 "$COPILOT_SQUEEZ_DIR" "$COPILOT_SQUEEZ_DIR/sessions" "$COPILOT_SQUEEZ_DIR/memory" 2>/dev/null || true
133138

134139
# Symlink the same binary so SQUEEZ_DIR-aware calls work
135-
ln -sf "$INSTALL_DIR/bin/squeez" "$COPILOT_SQUEEZ_DIR/bin/squeez" 2>/dev/null || \
136-
cp "$INSTALL_DIR/bin/squeez" "$COPILOT_SQUEEZ_DIR/bin/squeez"
140+
ln -sf "$INSTALL_DIR/bin/$BIN_NAME" "$COPILOT_SQUEEZ_DIR/bin/$BIN_NAME" 2>/dev/null || \
141+
cp "$INSTALL_DIR/bin/$BIN_NAME" "$COPILOT_SQUEEZ_DIR/bin/$BIN_NAME"
137142

138143
curl -fsSL "$REPO_RAW/hooks/copilot-pretooluse.sh" -o "$INSTALL_DIR/hooks/copilot-pretooluse.sh"
139144
curl -fsSL "$REPO_RAW/hooks/copilot-session-start.sh" -o "$INSTALL_DIR/hooks/copilot-session-start.sh"
@@ -143,7 +148,7 @@ chmod +x "$INSTALL_DIR/hooks/copilot-pretooluse.sh" \
143148
"$INSTALL_DIR/hooks/copilot-posttooluse.sh"
144149

145150
# Seed Copilot instructions (writes ~/.copilot/copilot-instructions.md)
146-
"$INSTALL_DIR/bin/squeez" init --copilot 2>/dev/null || true
151+
"$INSTALL_DIR/bin/$BIN_NAME" init --copilot 2>/dev/null || true
147152

148153
# Register hooks in ~/.copilot/settings.json (Copilot CLI hook format mirrors Claude Code)
149154
if [ -d "$HOME/.copilot" ]; then
@@ -183,7 +188,7 @@ os.replace(tmp, path)
183188
COPILOT_EOF
184189
fi
185190

186-
version=$("$INSTALL_DIR/bin/squeez" --version 2>/dev/null || echo "squeez")
191+
version=$("$INSTALL_DIR/bin/$BIN_NAME" --version 2>/dev/null || echo "squeez")
187192
echo "$version installed."
188193
echo ""
189194
echo "Claude Code: Restart Claude Code to activate."

src/commands/init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub fn run() -> i32 {
2121
/// ~/.copilot/copilot-instructions.md so Copilot CLI picks it up
2222
/// at every session start (no hook system required).
2323
pub fn run_copilot() -> i32 {
24-
let home = std::env::var("HOME").unwrap_or_default();
24+
let home = crate::session::home_dir();
2525
// Honour SQUEEZ_DIR override, default to ~/.copilot/squeez
2626
let base = std::env::var("SQUEEZ_DIR")
2727
.unwrap_or_else(|_| format!("{}/.copilot/squeez", home));

src/commands/wrap.rs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,35 @@ use crate::config::Config;
22
use crate::filter;
33
use crate::{json_util, session};
44
use std::io::Read;
5-
use std::os::unix::process::CommandExt;
65
use std::process::{Command, Stdio};
6+
#[cfg(unix)]
77
use std::sync::atomic::{AtomicI32, Ordering};
88
use std::thread;
99
use std::time::{Duration, Instant};
1010

11+
#[cfg(unix)]
1112
static CHILD_PID: AtomicI32 = AtomicI32::new(-1);
1213

14+
/// Returns a `Command` pre-configured to run `cmd` through the platform shell.
15+
/// Unix/Git Bash: `sh -c <cmd>`
16+
/// Windows native: `cmd /C <cmd>`
17+
fn shell_command(cmd: &str) -> Command {
18+
#[cfg(windows)]
19+
{
20+
let mut c = Command::new("cmd");
21+
c.args(["/C", cmd]);
22+
c
23+
}
24+
#[cfg(not(windows))]
25+
{
26+
let mut c = Command::new("sh");
27+
c.args(["-c", cmd]);
28+
c
29+
}
30+
}
31+
1332
pub fn run(cmd_str: &str) -> i32 {
33+
#[cfg(unix)]
1434
setup_signals();
1535
let config = Config::load();
1636

@@ -20,23 +40,24 @@ pub fn run(cmd_str: &str) -> i32 {
2040

2141
let start = Instant::now();
2242

23-
// Spawn via sh -c to handle pipes, &&, redirections, builtins
24-
let mut child = match Command::new("sh")
25-
.arg("-c")
26-
.arg(cmd_str)
27-
.stdout(Stdio::piped())
28-
.stderr(Stdio::piped())
29-
.process_group(0)
30-
.spawn()
43+
// Spawn via platform shell to handle pipes, &&, redirections, builtins
44+
let mut cmd = shell_command(cmd_str);
45+
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
46+
#[cfg(unix)]
3147
{
48+
use std::os::unix::process::CommandExt;
49+
cmd.process_group(0);
50+
}
51+
let mut child = match cmd.spawn() {
3252
Ok(c) => c,
3353
Err(e) => {
3454
eprintln!("squeez: {}", e);
3555
return 1;
3656
}
3757
};
3858

39-
// Store PID for signal forwarding
59+
// Store PID for signal forwarding (Unix only)
60+
#[cfg(unix)]
4061
CHILD_PID.store(child.id() as i32, Ordering::SeqCst);
4162

4263
// Drain stdout/stderr on background threads to prevent pipe-buffer deadlock.
@@ -76,10 +97,11 @@ pub fn run(cmd_str: &str) -> i32 {
7697
Ok(Some(s)) => break s.code().unwrap_or(1),
7798
Ok(None) => {
7899
if start.elapsed() >= timeout {
100+
#[cfg(unix)]
79101
unsafe {
80102
libc::kill(-(child.id() as i32), libc::SIGTERM);
103+
std::thread::sleep(Duration::from_millis(200));
81104
}
82-
std::thread::sleep(Duration::from_millis(200));
83105
let _ = child.kill();
84106
eprintln!("squeez: command timed out after 120s");
85107
let _ = stdout_thread.join();
@@ -148,9 +170,7 @@ pub fn run(cmd_str: &str) -> i32 {
148170
}
149171

150172
fn passthrough(cmd: &str) -> i32 {
151-
let status = Command::new("sh")
152-
.arg("-c")
153-
.arg(cmd)
173+
let status = shell_command(cmd)
154174
.status()
155175
.unwrap_or_else(|e| {
156176
eprintln!("squeez: {}", e);
@@ -166,13 +186,15 @@ fn is_streaming(cmd: &str) -> bool {
166186
&& cmd.split_whitespace().any(|a| a == "-f" || a == "--follow")
167187
}
168188

189+
#[cfg(unix)]
169190
fn setup_signals() {
170191
unsafe {
171192
libc::signal(libc::SIGTERM, forward_signal as *const () as libc::sighandler_t);
172193
libc::signal(libc::SIGINT, forward_signal as *const () as libc::sighandler_t);
173194
}
174195
}
175196

197+
#[cfg(unix)]
176198
extern "C" fn forward_signal(sig: libc::c_int) {
177199
let pid = CHILD_PID.load(Ordering::SeqCst);
178200
if pid > 0 {

src/config.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ impl Config {
7979

8080
pub fn load() -> Self {
8181
let base = std::env::var("SQUEEZ_DIR").unwrap_or_else(|_| {
82-
format!(
83-
"{}/.claude/squeez",
84-
std::env::var("HOME").unwrap_or_default()
85-
)
82+
format!("{}/.claude/squeez", crate::session::home_dir())
8683
});
8784
let path = format!("{}/config.ini", base);
8885
std::fs::read_to_string(&path)

src/session.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@ use std::path::{Path, PathBuf};
33

44
// ── Directory helpers ──────────────────────────────────────────────────────
55

6+
/// Returns the user's home directory.
7+
/// Tries `HOME` first (Unix/Git Bash), then `USERPROFILE` (Windows native).
8+
pub fn home_dir() -> String {
9+
std::env::var("HOME")
10+
.or_else(|_| std::env::var("USERPROFILE"))
11+
.unwrap_or_default()
12+
}
13+
614
/// Returns the squeez state directory. Overridable via SQUEEZ_DIR env var (for tests).
715
pub fn squeez_dir() -> PathBuf {
816
if let Ok(d) = std::env::var("SQUEEZ_DIR") {
917
return PathBuf::from(d);
1018
}
11-
PathBuf::from(format!(
12-
"{}/.claude/squeez",
13-
std::env::var("HOME").unwrap_or_default()
14-
))
19+
PathBuf::from(format!("{}/.claude/squeez", home_dir()))
1520
}
1621

1722
pub fn sessions_dir() -> PathBuf {

tests/test_session.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,10 @@ fn test_unix_to_date_epoch_zero() {
9999
let date = squeez::session::unix_to_date(0);
100100
assert_eq!(date, "1970-01-01", "got: {}", date);
101101
}
102+
103+
#[test]
104+
fn test_home_dir_returns_nonempty() {
105+
// HOME (Unix) or USERPROFILE (Windows) must be set in any real environment.
106+
let home = squeez::session::home_dir();
107+
assert!(!home.is_empty(), "home_dir() returned empty string");
108+
}

0 commit comments

Comments
 (0)