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 hooks/copilot-posttooluse.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
# squeez Copilot CLI PostToolUse hook — tracks token usage per tool call
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0

export SQUEEZ_DIR="$HOME/.copilot/squeez"
Expand Down
4 changes: 4 additions & 0 deletions hooks/copilot-pretooluse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
set -euo pipefail

SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0

export SQUEEZ_DIR="$HOME/.copilot/squeez"
Expand Down
4 changes: 4 additions & 0 deletions hooks/copilot-session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# Initialises the session and injects memory into ~/.copilot/copilot-instructions.md
# Run this once per session: add to shell RC or invoke manually.
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0

export SQUEEZ_DIR="$HOME/.copilot/squeez"
Expand Down
4 changes: 4 additions & 0 deletions hooks/posttooluse.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env bash
# squeez PostToolUse hook — tracks token usage per tool call
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0

input=$(cat)
Expand Down
4 changes: 4 additions & 0 deletions hooks/pretooluse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
set -euo pipefail

SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0

SQUEEZ_BIN="$SQUEEZ" python3 -c "
Expand Down
4 changes: 4 additions & 0 deletions hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
# squeez SessionStart hook — runs squeez init, prints memory banner to session context
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
if [ ! -x "$SQUEEZ" ]; then
_sq=$(command -v squeez 2>/dev/null || true)
[ -n "$_sq" ] && SQUEEZ="$_sq"
fi
[ ! -x "$SQUEEZ" ] && exit 0
"$SQUEEZ" init
73 changes: 48 additions & 25 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ REPO_RAW="https://raw.githubusercontent.com/claudioemmanuel/squeez/main"
RELEASES="https://github.com/claudioemmanuel/squeez/releases/latest/download"
INSTALL_DIR="$HOME/.claude/squeez"

# Parse flags
SETUP_ONLY=0
for arg in "$@"; do
case "$arg" in
--setup-only) SETUP_ONLY=1 ;;
esac
done

# Detect OS and architecture
OS=$(uname -s)
ARCH=$(uname -m)
Expand Down Expand Up @@ -32,35 +40,50 @@ esac
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/hooks" "$INSTALL_DIR/sessions" "$INSTALL_DIR/memory"
chmod 700 "$INSTALL_DIR" "$INSTALL_DIR/sessions" "$INSTALL_DIR/memory" 2>/dev/null || true

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

echo "Verifying checksum..."
curl -fsSL "$RELEASES/checksums.sha256" -o /tmp/squeez-checksums.sha256
expected=$(grep "$BINARY" /tmp/squeez-checksums.sha256 2>/dev/null | awk '{print $1}')
rm -f /tmp/squeez-checksums.sha256
if [ -z "$expected" ]; then
echo "ERROR: could not find checksum for $BINARY in release" >&2
rm -f "$INSTALL_DIR/bin/$BIN_NAME"
if [ "$SETUP_ONLY" -eq 1 ]; then
# --setup-only: skip download, use existing squeez binary from PATH
echo "Setup-only mode: skipping binary download..."
EXISTING=$(command -v squeez 2>/dev/null || true)
if [ -z "$EXISTING" ]; then
echo "ERROR: squeez not found in PATH." >&2
echo "Install first with: cargo install squeez" >&2
exit 1
fi

# Use sha256sum if available (Linux/Windows Git Bash), otherwise fall back to shasum (macOS)
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
fi
echo "Found squeez at: $EXISTING"
cp "$EXISTING" "$INSTALL_DIR/bin/$BIN_NAME"
chmod +x "$INSTALL_DIR/bin/$BIN_NAME" 2>/dev/null || true
echo "Binary copied to $INSTALL_DIR/bin/$BIN_NAME"
else
actual=$(shasum -a 256 "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
echo "Downloading squeez binary for $OS/$ARCH..."
curl -fsSL "$RELEASES/$BINARY" -o "$INSTALL_DIR/bin/$BIN_NAME"

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

# Use sha256sum if available (Linux/Windows Git Bash), otherwise fall back to shasum (macOS)
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
else
actual=$(shasum -a 256 "$INSTALL_DIR/bin/$BIN_NAME" | awk '{print $1}')
fi

if [ "$expected" != "$actual" ]; then
echo "ERROR: checksum mismatch — binary may be corrupted or tampered" >&2
rm -f "$INSTALL_DIR/bin/$BIN_NAME"
exit 1
fi
echo "Checksum verified."

chmod +x "$INSTALL_DIR/bin/$BIN_NAME" 2>/dev/null || true
fi

if [ "$expected" != "$actual" ]; then
echo "ERROR: checksum mismatch — binary may be corrupted or tampered" >&2
rm -f "$INSTALL_DIR/bin/$BIN_NAME"
exit 1
fi
echo "Checksum verified."

chmod +x "$INSTALL_DIR/bin/$BIN_NAME" 2>/dev/null || true

echo "Installing hooks..."
curl -fsSL "$REPO_RAW/hooks/pretooluse.sh" -o "$INSTALL_DIR/hooks/pretooluse.sh"
curl -fsSL "$REPO_RAW/hooks/session-start.sh" -o "$INSTALL_DIR/hooks/session-start.sh"
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ pub mod track;
pub mod track_result;
pub mod typescript;
pub mod benchmark;
pub mod setup;
pub mod update;
pub mod wrap;
229 changes: 229 additions & 0 deletions src/commands/setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// `squeez setup` — post-install setup for cargo/npm users.
//
// Creates ~/.claude/squeez/ directory structure, copies the running binary
// to the canonical hooks location, downloads hook scripts from GitHub, and
// registers hooks + statusline in ~/.claude/settings.json.

use std::path::PathBuf;

use crate::session::home_dir;

const REPO_RAW: &str = "https://raw.githubusercontent.com/claudioemmanuel/squeez/main";

const HOOKS: &[&str] = &[
"pretooluse.sh",
"session-start.sh",
"posttooluse.sh",
"copilot-pretooluse.sh",
"copilot-session-start.sh",
"copilot-posttooluse.sh",
];

// Python script that registers squeez hooks + statusline in ~/.claude/settings.json.
// Mirrors the registration block in install.sh.
const REGISTER_SETTINGS_PY: &str = r#"
import json, os, sys

path = os.path.expanduser("~/.claude/settings.json")
settings = {}
try:
if os.path.exists(path):
with open(path) as f:
settings = json.load(f)
except (json.JSONDecodeError, IOError) as e:
print("Warning: could not read settings.json: " + str(e), file=sys.stderr)

def ensure_list(key):
if not isinstance(settings.get(key), list):
settings[key] = []

ensure_list("PreToolUse")
pre = {"matcher": "Bash", "hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/pretooluse.sh"}]}
if not any("squeez" in str(h) for h in settings["PreToolUse"]):
settings["PreToolUse"].append(pre)

ensure_list("SessionStart")
start = {"hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/session-start.sh"}]}
if not any("squeez" in str(h) for h in settings["SessionStart"]):
settings["SessionStart"].append(start)

ensure_list("PostToolUse")
post = {"hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/posttooluse.sh"}]}
if not any("squeez" in str(h) for h in settings["PostToolUse"]):
settings["PostToolUse"].append(post)

existing_status = settings.get("statusLine", {})
existing_cmd = existing_status.get("command", "") if isinstance(existing_status, dict) else ""
squeez_cmd = "bash ~/.claude/squeez/bin/statusline.sh"
if "squeez" not in existing_cmd:
if existing_cmd:
new_cmd = "bash -c 'input=$(cat); echo \"$input\" | { " + existing_cmd.rstrip() + "; } 2>/dev/null; echo \"$input\" | " + squeez_cmd + "'"
settings["statusLine"] = {"type": "command", "command": new_cmd}
else:
settings["statusLine"] = {"type": "command", "command": squeez_cmd}

os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(settings, f, indent=2)
os.replace(tmp, path)
print("settings.json updated.")
"#;

pub fn run(args: &[String]) -> i32 {
let force = args.iter().any(|a| a == "--force" || a == "-f");

let home = home_dir();
let install_dir = format!("{}/.claude/squeez", home);

// 1. Create directory structure
for sub in &["bin", "hooks", "sessions", "memory"] {
let path = format!("{}/{}", install_dir, sub);
if let Err(e) = std::fs::create_dir_all(&path) {
eprintln!("squeez setup: failed to create {}: {}", path, e);
return 1;
}
}

// 2. Copy self binary to canonical hooks location
let bin_name = if cfg!(windows) { "squeez.exe" } else { "squeez" };
let target_bin = PathBuf::from(format!("{}/bin/{}", install_dir, bin_name));

let current_exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!("squeez setup: cannot determine current exe path: {}", e);
return 1;
}
};

let already_in_place = current_exe
.canonicalize()
.ok()
.zip(target_bin.canonicalize().ok())
.map(|(a, b)| a == b)
.unwrap_or(false);

if !already_in_place || force {
if let Err(e) = std::fs::copy(&current_exe, &target_bin) {
eprintln!("squeez setup: failed to copy binary to {}: {}", target_bin.display(), e);
return 1;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&target_bin, std::fs::Permissions::from_mode(0o755));
}
println!("squeez setup: binary installed → {}", target_bin.display());
} else {
println!("squeez setup: binary already at {}", target_bin.display());
}

// 3. Download hook scripts from GitHub
println!("squeez setup: downloading hooks...");
for hook in HOOKS {
let url = format!("{}/hooks/{}", REPO_RAW, hook);
let dest = format!("{}/hooks/{}", install_dir, hook);
match crate::commands::update::curl(&url) {
Ok(bytes) => {
if let Err(e) = std::fs::write(&dest, &bytes) {
eprintln!("squeez setup: failed to write hook {}: {}", hook, e);
return 1;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755));
}
}
Err(e) => {
eprintln!("squeez setup: failed to download hook {}: {}", hook, e);
return 1;
}
}
}

// 4. Download statusline.sh
let statusline_url = format!("{}/scripts/statusline.sh", REPO_RAW);
let statusline_dest = format!("{}/bin/statusline.sh", install_dir);
match crate::commands::update::curl(&statusline_url) {
Ok(bytes) => {
if let Err(e) = std::fs::write(&statusline_dest, &bytes) {
eprintln!("squeez setup: warning: could not write statusline.sh: {}", e);
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&statusline_dest, std::fs::Permissions::from_mode(0o755));
}
}
}
Err(e) => eprintln!("squeez setup: warning: could not download statusline.sh: {}", e),
}

// 5. Register hooks in ~/.claude/settings.json
println!("squeez setup: registering hooks in settings.json...");
if let Err(e) = register_claude_settings() {
eprintln!("squeez setup: failed to update settings.json: {}", e);
return 1;
}

let version = crate::commands::update::current_version();
println!("squeez setup: done — squeez {} ready. Restart Claude Code to activate.", version);
0
}

/// Registers squeez hooks and statusline in ~/.claude/settings.json.
/// Called by both `squeez setup` and `squeez update`.
pub fn register_claude_settings() -> Result<(), String> {
run_python(REGISTER_SETTINGS_PY)
}

fn run_python(script: &str) -> Result<(), String> {
use std::io::Write;

// Try python3 first (Unix/modern Windows), then python (older Windows)
for python in &["python3", "python"] {
let mut child = match std::process::Command::new(python)
.arg("-")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
{
Ok(c) => c,
Err(_) => continue,
};

if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(script.as_bytes());
}

let status = child.wait().map_err(|e| e.to_string())?;
if status.success() {
return Ok(());
}
return Err("python script exited with error".to_string());
}

Err("python3/python not found — cannot update settings.json".to_string())
}

fn print_help() {
println!("squeez setup — configure hooks after cargo/npm install");
println!();
println!("Usage:");
println!(" squeez setup Install hooks and register in settings.json");
println!(" squeez setup --force Force re-copy binary even if already in place");
println!();
println!("Use this after: cargo install squeez OR npm i -g squeez");
println!("Equivalent to running: curl -fsSL <install.sh url> | sh -s -- --setup-only");
}

pub fn run_with_help(args: &[String]) -> i32 {
if args.iter().any(|a| a == "-h" || a == "--help") {
print_help();
return 0;
}
run(args)
}
Loading
Loading