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
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.

44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,40 @@

## 📦 Installation

### Cargo (Recommended)
### Quick Install (Linux/macOS)

```bash
cargo install cora-cli
curl -fsSL https://raw.githubusercontent.com/codecoradev/cora-cli/main/install.sh | sh
```

### Binary Download

Download the latest release from [GitHub Releases](https://github.com/codecoradev/cora-cli/releases):
Installs to `~/.local/bin`. Add to PATH if needed:

```bash
# Determine your platform tag from the releases page, e.g.:
# cora-aarch64-unknown-linux-gnu-v0.2.0.tar.gz
# cora-x86_64-unknown-linux-gnu-v0.2.0.tar.gz
# cora-aarch64-apple-darwin-v0.2.0.tar.gz
# cora-x86_64-pc-windows-msvc-v0.2.0.zip

# Example: Linux aarch64
VERSION=$(curl -s https://api.github.com/repos/codecoradev/cora-cli/releases/latest | grep tag_name | cut -d'"' -f4)
curl -L "https://github.com/codecoradev/cora-cli/releases/download/${VERSION}/cora-aarch64-unknown-linux-gnu-${VERSION}.tar.gz" | tar xz
sudo mv cora /usr/local/bin/
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
```

> **Tip:** Visit the [Releases page](https://github.com/codecoradev/cora-cli/releases) to find the correct asset name for your platform.
### Pre-built Binaries

Download from [GitHub Releases](https://github.com/codecoradev/cora-cli/releases):

| Platform | Asset |
|----------|-------|
| Linux x86_64 | `cora-x86_64-unknown-linux-gnu-v*.tar.gz` |
| Linux ARM64 | `cora-aarch64-unknown-linux-gnu-v*.tar.gz` |
| macOS Apple Silicon | `cora-aarch64-apple-darwin-v*.tar.gz` |
| Windows x86_64 | `cora-x86_64-pc-windows-msvc-v*.zip` |

> **Windows users:** Extract the zip and place `cora.exe` somewhere in your PATH (e.g. `C:\Users\<you>\.local\bin`). Run from Command Prompt, PowerShell, or Windows Terminal — do not double-click the `.exe` (it will flash and close).

### Cargo

```bash
cargo install --git https://github.com/codecoradev/cora-cli
```

### Homebrew

> 🚧 Homebrew tap is planned — check back soon!
> 🚧 Homebrew tap is planned — see [#151](https://github.com/codecoradev/cora-cli/issues/151).

### Build from Source

Expand Down Expand Up @@ -129,6 +135,9 @@ cora review --base origin/main
# Review a pull request diff from a file
cora review --diff-file pr.diff

# CI mode: skip diff size limit, hard gate on any findings
cora review --ci --base ${{ github.base_ref }}

# Use a specific model
cora review --model gpt-4o

Expand Down Expand Up @@ -283,6 +292,7 @@ hook:
mode: warn # warn | block
min_severity: major # info | minor | major | critical
max_diff_size: 51200 # Max diff size in bytes (50 KB)
on_violation: warn # warn | disallow — block commit on oversized diff

# Output settings
output:
Expand Down
174 changes: 174 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env sh
# cora installer — https://github.com/codecoradev/cora-cli
# Usage: curl -fsSL https://raw.githubusercontent.com/codecoradev/cora-cli/main/install.sh | sh

set -e

REPO="codecoradev/cora-cli"
BINARY_NAME="cora"
INSTALL_DIR="${CORA_INSTALL_DIR:-$HOME/.local/bin}"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

info() {
printf "${GREEN}[INFO]${NC} %s\n" "$1"
}

warn() {
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
}

error() {
printf "${RED}[ERROR]${NC} %s\n" "$1"
exit 1
}

# Detect OS
detect_os() {
case "$(uname -s)" in
Linux*) OS="linux";;
Darwin*) OS="darwin";;
*) error "Unsupported operating system: $(uname -s)";;
esac
}

# Detect architecture
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) ARCH="x86_64";;
arm64|aarch64) ARCH="aarch64";;
*) error "Unsupported architecture: $(uname -m)";;
esac
}

# Get latest release version
# Primary: parse the 302 redirect on /releases/latest (no API call, no rate limit).
# Fallback: the GitHub REST API (subject to 60 req/hour anonymous limit).
get_latest_version() {
# Try the web redirect first — does not count against the API rate limit.
VERSION=$(curl -sI "https://github.com/${REPO}/releases/latest" \
| grep -i '^location:' \
| sed -E 's|.*/tag/([^[:space:]]+).*|\1|' \
| tr -d '\r')

# Fallback to the REST API if the redirect didn't yield a tag.
if [ -z "$VERSION" ]; then
warn "Redirect lookup failed, falling back to GitHub API..."
VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name":' \
| sed -E 's/.*"([^"]+)".*/\1/')
fi

if [ -z "$VERSION" ]; then
error "Failed to get latest version. Set CORA_VERSION=vX.Y.Z to pin a version."
fi
}

# Build target triple and archive name
get_target() {
case "$OS" in
linux)
case "$ARCH" in
x86_64) TARGET="x86_64-unknown-linux-gnu";;
aarch64) TARGET="aarch64-unknown-linux-gnu";;
esac
;;
darwin)
# Only aarch64 (Apple Silicon) is currently published
if [ "$ARCH" != "aarch64" ]; then
warn "No pre-built binary for x86_64 macOS. Install via cargo:"
warn " cargo install --git https://github.com/${REPO}"
exit 0
fi
TARGET="aarch64-apple-darwin"
;;
esac
}

# Download and install
install() {
info "Detected: $OS $ARCH"
info "Target: $TARGET"
info "Version: $VERSION"

ARCHIVE_NAME="${BINARY_NAME}-${TARGET}-${VERSION}.tar.gz"
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ARCHIVE_NAME}"
TEMP_DIR=$(mktemp -d)
ARCHIVE="${TEMP_DIR}/${ARCHIVE_NAME}"

info "Downloading from: $DOWNLOAD_URL"
if ! curl -fsSL "$DOWNLOAD_URL" -o "$ARCHIVE"; then
error "Failed to download ${ARCHIVE_NAME}"
fi

# Verify archive contents before extraction (CWE-22 path traversal).
# Reject any entry with an absolute path or a ".." component.
info "Verifying archive..."
if tar -tzf "$ARCHIVE" | grep -qE '^/|(^|/)\.\.(/|$)'; then
error "Archive contains unsafe paths (absolute or directory traversal) — refusing to extract"
fi

info "Extracting..."
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"

mkdir -p "$INSTALL_DIR"
mv "${TEMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/"

chmod +x "${INSTALL_DIR}/${BINARY_NAME}"

# Cleanup
rm -rf "$TEMP_DIR"

info "Successfully installed ${BINARY_NAME} to ${INSTALL_DIR}/${BINARY_NAME}"
}

# Verify installation
verify() {
if command -v "$BINARY_NAME" >/dev/null 2>&1; then
INSTALLED_VERSION=$("$BINARY_NAME" --version 2>/dev/null || echo "unknown")
info "Verification: $INSTALLED_VERSION"
else
warn "Binary installed but not in PATH. Add to your shell profile:"
case "${SHELL:-}" in
*/zsh)
warn ' echo '\''export PATH="$HOME/.local/bin:$PATH"'\'' >> ~/.zshrc'
warn ' source ~/.zshrc'
;;
*/bash)
warn ' echo '\''export PATH="$HOME/.local/bin:$PATH"'\'' >> ~/.bashrc'
warn ' source ~/.bashrc'
;;
*/fish)
warn ' fish_add_path ~/.local/bin'
;;
*)
warn ' export PATH="$HOME/.local/bin:$PATH"'
;;
esac
fi
}

main() {
info "Installing ${BINARY_NAME}..."

detect_os
detect_arch
get_target
if [ -n "$CORA_VERSION" ]; then
VERSION="$CORA_VERSION"
info "Using pinned version from CORA_VERSION: $VERSION"
else
get_latest_version
fi
install
verify

echo ""
info "Installation complete! Run '${BINARY_NAME} --help' to get started."
}

main
33 changes: 30 additions & 3 deletions src/commands/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
pub severity: Option<String>,
/// Disable review caching.
pub no_cache: bool,
/// CI mode: skip diff size limit, hard gate on any findings.
pub ci: bool,
}

/// Result of a review command execution.
Expand Down Expand Up @@ -82,9 +84,27 @@
progress.parsing_diff(files_changed, lines_changed);
}

// 3. Validate size
// 3. Validate size (skip in CI mode)
let max_size = opts.max_diff_size.unwrap_or(config.hook.max_diff_size);
if diff.len() > max_size {
if !opts.ci && diff.len() > max_size {
if config.hook.on_violation == "disallow" {
// Return blocked result instead of error
return Ok(ReviewResult {
exit_code: EXIT_BLOCKED,
output: format!(
"{}\n",
format!(
"❌ Diff too large ({} chars, max {}). Commit blocked. \
Use --base to review a specific branch, increase \
hook.max_diff_size, or run: git commit --no-verify",
diff.len(),
max_size
)
.red()
.bold(),
),
});
}
anyhow::bail!(
"Diff too large ({} chars, max {}). Use --base to review a specific branch, or increase hook.max_diff_size.",
diff.len(),
Expand Down Expand Up @@ -156,7 +176,14 @@
let output = formatter.format_review(&filtered_response)?;

// 8. Return exit code
let exit_code = if response.should_block && config.hook.mode == "block" {
let exit_code = if opts.ci {
// CI mode: hard gate — any finding blocks
if !filtered_response.issues.is_empty() {
EXIT_BLOCKED
} else {
EXIT_OK
}
} else if response.should_block && config.hook.mode == "block" {
EXIT_BLOCKED
} else {
EXIT_OK
Expand Down
12 changes: 12 additions & 0 deletions src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub struct HookConfig {
pub mode: String,
pub min_severity: String,
pub max_diff_size: usize,
pub on_violation: String, // "warn" | "disallow"
}

/// Output configuration.
Expand Down Expand Up @@ -105,6 +106,7 @@ impl Default for Config {
mode: "warn".to_string(),
min_severity: "major".to_string(),
max_diff_size: 50 * 1024,
on_violation: "warn".to_string(),
},
output: OutputConfig {
format: "pretty".to_string(),
Expand Down Expand Up @@ -187,6 +189,8 @@ pub struct HookSection {
pub min_severity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_diff_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_violation: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -326,6 +330,9 @@ impl CoraFile {
if let Some(v) = h.max_diff_size {
config.hook.max_diff_size = v;
}
if let Some(v) = &h.on_violation {
config.hook.on_violation.clone_from(v);
}
}
if let Some(o) = &self.output {
if let Some(v) = &o.format {
Expand Down Expand Up @@ -443,6 +450,7 @@ mod tests {
assert_eq!(cfg.hook.mode, "warn");
assert_eq!(cfg.hook.min_severity, "major");
assert_eq!(cfg.hook.max_diff_size, 50 * 1024);
assert_eq!(cfg.hook.on_violation, "warn");
}

#[test]
Expand Down Expand Up @@ -543,13 +551,15 @@ mod tests {
mode: Some("block".to_string()),
min_severity: Some("critical".to_string()),
max_diff_size: Some(1024),
on_violation: Some("disallow".to_string()),
}),
..Default::default()
};
cora.merge_into(&mut cfg);
assert_eq!(cfg.hook.mode, "block");
assert_eq!(cfg.hook.min_severity, "critical");
assert_eq!(cfg.hook.max_diff_size, 1024);
assert_eq!(cfg.hook.on_violation, "disallow");
}

#[test]
Expand Down Expand Up @@ -620,6 +630,7 @@ output:
mode: "warn".to_string(),
min_severity: "critical".to_string(),
max_diff_size: 1024,
on_violation: "warn".to_string(),
};
assert_eq!(cfg.min_severity_level(), Severity::Critical);
}
Expand All @@ -630,6 +641,7 @@ output:
mode: "warn".to_string(),
min_severity: "whatever".to_string(),
max_diff_size: 1024,
on_violation: "warn".to_string(),
};
assert_eq!(cfg.min_severity_level(), Severity::Info);
}
Expand Down
Loading
Loading