diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 8b5ec47..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM rust:1-slim-bookworm - -# Set UTF-8 locale -ENV LANG=C.UTF-8 \ - LANGUAGE=C.UTF-8 \ - LC_ALL=C.UTF-8 - -# Install Node.js and other dependencies as root -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends \ - curl \ - git \ - pkg-config \ - libssl-dev \ - ripgrep \ - jq \ - sudo \ - build-essential \ - chromium \ - lcov \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* - -# Install Node.js LTS manually -RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ - && apt-get install -y nodejs \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* - -# Ensure pre-installed tools are always in PATH even if CARGO_HOME is overridden at runtime -ENV PATH=/usr/local/cargo/bin:$PATH - -# Install cargo-binstall and other rust tools as root (installs to /usr/local/cargo/bin) -RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \ - && rustup component add rustfmt clippy llvm-tools-preview \ - && cargo binstall -y cargo-audit cargo-llvm-cov diff --git a/.devcontainer/README.md b/.devcontainer/README.md deleted file mode 100644 index ace5e4c..0000000 --- a/.devcontainer/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Dev Container - -This folder contains the development container configuration for the Arxiv-CLI project. - -## CLI Usage - -When using the `devcontainer` CLI, version **0.80.2** or later is required. - -```bash -npx -y @devcontainers/cli@0.80.2 up --workspace-folder . --remove-existing-container -``` - -**Important:** Earlier versions (including 0.80.0) have a bug that causes the CLI to hang after "Container started". - -## Files - -- `devcontainer.json` - Container configuration -- `Dockerfile` - Base image with Rust, Node.js, and development tools -- `post-create.sh` - Setup script that runs in background after container creation - -## Setup Process - -The `post-create.sh` script runs automatically after the container starts. It: - -1. Fixes permissions for `CARGO_HOME` -2. Installs Claude CLI -3. Configures `tmux` -4. Runs `cargo check` to verify the project -5. Installs the `arxiv-cli` binary -6. Configures the `claude` alias -7. Authenticates with Z.ai (if `Z_AI_API_KEY` is set) - -The `devcontainer up` command will wait for the setup to complete before exiting. - -## CI Environment - -In CI (when `CI` or `GITHUB_ACTIONS` is set), the setup script skips all development setup steps. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d4cdf3a..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "Chrome-CDP Dev", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - }, - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "true", - "username": "vscode", - "userUid": "1000", - "userGid": "1000", - "upgradePackages": "false", - }, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/git:1": {}, - }, - "mounts": [ - "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,consistency=cached", - ], - "customizations": { - "vscode": {}, - }, - "containerEnv": { - "Z_AI_API_KEY": "${localEnv:Z_AI_API_KEY}", - "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", - }, - "postCreateCommand": "bash .devcontainer/post-create.sh", - "remoteUser": "vscode", -} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100644 index 59340b1..0000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -if [ -z "$CI" ] && [ -z "$GITHUB_ACTIONS" ]; then - # Fix permissions for local development where CARGO_HOME is root-owned by the base image - sudo chown -R vscode:vscode /usr/local/cargo - - # Install Claude CLI as vscode user if not already installed - if ! command -v claude >/dev/null 2>&1; then - echo "[Devcontainer Setup] Installing Claude CLI..." - curl -fsSL https://claude.ai/install.sh | bash - - # Add .local/bin to PATH for current session - export PATH="$HOME/.local/bin:$PATH" - - # Add to shell configs for future sessions - echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.bashrc - echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.zshrc - else - echo "[Devcontainer Setup] Claude CLI already installed: $(claude --version)" - fi - - echo "[Devcontainer Setup] Configuring claude alias..." - echo 'alias claude="claude --allow-dangerously-skip-permissions"' >> $HOME/.bashrc - echo 'alias claude="claude --allow-dangerously-skip-permissions"' >> $HOME/.zshrc - - # Install mise as vscode user - if ! command -v mise >/dev/null 2>&1; then - echo "[Devcontainer Setup] Installing mise..." - curl https://mise.run | sh - export PATH="$HOME/.local/bin:$PATH" - fi - - echo "[Devcontainer Setup] Configuring mise..." - echo 'eval "$(mise activate bash)"' >> $HOME/.bashrc - echo 'eval "$(mise activate zsh)"' >> $HOME/.zshrc - - # Run mise install - if command -v mise >/dev/null 2>&1; then - echo "[Devcontainer Setup] Installing tools with mise..." - mise trust - mise install - - echo "[Devcontainer Setup] Setting up git pre-commit hook..." - mise generate git-pre-commit --write --task=pre-commit - else - echo "[Devcontainer Setup] WARNING: mise is not installed." - fi - - echo "[Devcontainer Setup] Authenticating claude..." - if [ -n "$Z_AI_API_KEY" ]; then - mkdir -p "$HOME/.claude" - cat > "$HOME/.claude/settings.json" < "$HOME/.config/arxiv-cli/config.toml" <<'EOF' -headless = true -browser_path = "/usr/bin/chromium" -chrome_args = ["--no-sandbox", "--disable-gpu"] -EOF - - # Install skill-bench - if ! command -v skill-bench >/dev/null 2>&1; then - echo "[Devcontainer Setup] Installing skill-bench..." - curl -fsSL https://raw.githubusercontent.com/sonesuke/skill-bench/main/scripts/setup.sh | sh - else - echo "[Devcontainer Setup] skill-bench already installed: $(skill-bench --version 2>/dev/null || echo 'unknown')" - fi - - echo "[Devcontainer Setup] Complete!" -else - echo "Running in CI environment, skipping development setup..." -fi diff --git a/.gitignore b/.gitignore index 08afa61..1bca5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ agents/pr-healer/healer.log agents/pr-healer/progress.jsonl # Claude worktrees .claude/worktrees/ +logs/ +.skill-bench/ + +# Nix build artifacts +result diff --git a/AGENTS.md b/AGENTS.md index 3abfbdb..0271cee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,9 @@ agents/pr-healer/ # PR-Healer autonomous agent tools/ # Agent tools load-progress.sh # Read past context (JSONL) record-progress.sh # Write progress logs (JSONL) +scripts/ # Build and setup scripts (build.sh, up.sh, setup.sh) +flake.nix # Nix flake for reproducible Docker image mise.toml # Task definitions (fmt, clippy, test, pre-commit) -.devcontainer/ # Dev container configuration ``` ## Tools @@ -51,11 +52,21 @@ mise.toml # Task definitions (fmt, clippy, test, pre-commit) | `mise run coverage` | Measure code coverage (including subprocesses) | | `mise run skill-test` | Run all skill-bench tests | +## Development Container + +The dev environment uses a Nix flake-based Docker image managed via mise tasks. + +- **Build**: `mise run build` — Build the Docker image with Nix +- **Start**: `mise run up` — Start the dev container +- **Setup**: `mise run setup` — Configure git, Rust, Claude CLI, MCP tools, and skills inside the container +- **Attach**: `mise run attach` — Open a shell inside the running container +- **Stop**: `mise run down` — Stop and remove the container + ## Skill-Bench Testing Framework Test cases are in `tests/`. -Requires [skill-bench](https://github.com/sonesuke/skill-bench) (set up via post-create script). +Requires [skill-bench](https://github.com/sonesuke/skill-bench) (set up via `mise run setup`). ```toml name = "test-name" diff --git a/Cargo.lock b/Cargo.lock index a6a294a..6673985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arxiv-cli" -version = "0.1.5" +version = "0.2.0" dependencies = [ "anyhow", "assert_cmd", @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "cypher-rs" version = "0.1.0" -source = "git+https://github.com/sonesuke/cypher-rs?branch=main#ab6fdb71e5f7f8e8f1a04b3afcd707b724fb76b0" +source = "git+https://github.com/sonesuke/cypher-rs?branch=main#0f8d18798a195aaef63eae4787311b6a6c1652a3" dependencies = [ "anyhow", "async-trait", diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1d78474 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775877051, + "narHash": "sha256-wpSQm2PD/w4uRo2wb8utk0b5hOBkkg/CZ1xICY+qB7M=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "08b4f3633471874c8894632ade1b78d75dbda002", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b65506b --- /dev/null +++ b/flake.nix @@ -0,0 +1,108 @@ +{ + description = "arXiv CLI dev environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, rust-overlay }: + let + forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ]; + in + { + packages = forAllSystems (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + config.allowUnfreePredicate = pkg: builtins.elem (nixpkgs.lib.getName pkg) [ "chromium" ]; + }; + + devPackages = with pkgs; [ + bashInteractive + zsh + zsh-completions + zsh-autosuggestions + zsh-syntax-highlighting + coreutils + findutils + gnugrep + gnutar + gzip + gnused + curl + gitMinimal + gh + cacert + ripgrep + unzip + jq + vim + nodejs_22 + sqlite + chromium + python3 + perl + gnumake + gcc + pkg-config + openssl.dev + lcov + # Chromium runtime dependencies + fontconfig + dbus + liberation_ttf + (rust-bin.stable.latest.minimal.override { + extensions = [ "rustfmt-preview" "clippy-preview" ]; + }) + cargo-binstall + ]; + in + { + default = pkgs.dockerTools.buildLayeredImage { + name = "arxiv-cli"; + tag = "latest"; + contents = pkgs.buildEnv { + name = "image-root"; + paths = devPackages; + pathsToLink = [ "/bin" "/etc" "/lib" "/share" ]; + }; + fakeRootCommands = '' + mkdir -p ./home/user/.config ./workspaces ./tmp ./lib + chmod 1777 ./tmp + echo "user:x:1000:1000::/home/user:/bin/zsh" >> ./etc/passwd + echo "user:x:1000:" >> ./etc/group + chown -R 1000:1000 ./home/user + chmod 755 ./home/user + mkdir -p ./usr/bin + ln -sf /bin/env ./usr/bin/env + # Symlink chromium as google-chrome for compatibility + ln -sf /bin/chromium ./bin/google-chrome + for f in ${pkgs.glibc}/lib/ld-linux*.so*; do + ln -sf "$f" ./lib/$(basename "$f") + done + # Fontconfig setup for Chromium + mkdir -p ./etc/fonts + ln -sf ${pkgs.fontconfig.out}/etc/fonts/fonts.conf ./etc/fonts/fonts.conf + fc-cache -f 2>/dev/null || true + ''; + config = { + Env = [ + "LANG=C.UTF-8" + "LANGUAGE=C.UTF-8" + "LC_ALL=C.UTF-8" + "NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "HOME=/home/user" + ]; + User = "1000:1000"; + Cmd = [ "/bin/zsh" ]; + }; + }; + } + ); + }; +} diff --git a/mise.toml b/mise.toml index 0c4a63c..27ce531 100644 --- a/mise.toml +++ b/mise.toml @@ -1,16 +1,41 @@ +[tasks.build] +description = "Build dev container image with nix" +file = "scripts/build.sh" + +[tasks.up] +description = "Start dev container" +file = "scripts/up.sh" + +[tasks.down] +description = "Stop and remove dev container" +run = "docker stop arxiv-cli && docker rm arxiv-cli" + +[tasks.attach] +description = "Attach to dev container" +run = "docker exec -it -w /workspaces/arxiv-cli arxiv-cli /bin/zsh" + +[tasks.setup] +description = "Setup environment inside running container (Rust, Claude CLI, etc.)" +run = "docker exec -u 1000 arxiv-cli bash /workspaces/arxiv-cli/scripts/setup.sh" + [tasks.fmt] +description = "Check formatting with cargo fmt" run = "cargo fmt --all -- --check" [tasks.clippy] +description = "Lint with cargo clippy" run = "cargo clippy --all-targets -- -D warnings" [tasks.test] +description = "Run tests with cargo test" run = "RUSTFLAGS=\"-D warnings\" cargo test --all-targets" [tasks.pre-commit] +description = "Run all of the above" depends = ["fmt", "clippy", "test"] [tasks.coverage] +description = "Generate test coverage report" run = """ eval "$(cargo llvm-cov show-env --sh)" cargo llvm-cov clean --workspace @@ -19,4 +44,5 @@ cargo llvm-cov report --summary-only """ [tasks.skill-test] +description = "Run skill-bench tests" run = "cargo install --path . && skill-bench run tests --plugin-dir claude-plugin" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..3a808d0 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +ARCH=$(uname -m) +if [ "$ARCH" = "arm64" ]; then + NIX_SYSTEM="aarch64-linux" +else + NIX_SYSTEM="x86_64-linux" +fi + +NIX_FLAGS="--extra-experimental-features 'nix-command flakes'" + +docker volume create nix-store 2>/dev/null || true + +docker run --rm \ + -v "$(pwd):/workspace" \ + -v nix-store:/nix \ + -w /workspace \ + nixos/nix \ + sh -c " + git config --global --add safe.directory /workspace + nix $NIX_FLAGS build --no-link .#packages.${NIX_SYSTEM}.default + cat \$(nix $NIX_FLAGS path-info .#packages.${NIX_SYSTEM}.default) + " \ + | docker load diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..e23c070 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,106 @@ +#!/bin/bash +set -e + +# Configure git using GitHub noreply email and credential helper +if command -v gh >/dev/null 2>&1 && gh auth status &>/dev/null; then + gh auth setup-git + GH_USER=$(gh api user --jq .login) + GH_ID=$(gh api user --jq .id) + git config --global user.name "$GH_USER" + git config --global user.email "${GH_ID}+${GH_USER}@users.noreply.github.com" + echo "Git configured as $GH_USER (noreply email)" +else + echo "Warning: GitHub CLI not authenticated, skipping git config" +fi + +# Install cargo tools (Rust is pre-installed in the image) +echo "Installing cargo tools..." +cargo binstall -y cargo-audit cargo-llvm-cov + +# Install Claude CLI +if ! command -v claude >/dev/null 2>&1; then + echo "Installing Claude CLI..." + curl -fsSL https://claude.ai/install.sh | bash + export PATH="$HOME/.local/bin:$PATH" +else + echo "Claude CLI already installed: $(claude --version)" +fi + +# Configure Claude +if [ -n "$Z_AI_API_KEY" ]; then + mkdir -p "$HOME/.claude" + cat > "$HOME/.claude/settings.json" </dev/null | head -1) +SYNTAX_HIGHLIGHTING=$(find / -path "*/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 2>/dev/null | head -1) + +cat > "$HOME/.zshrc" </dev/null) || return + echo " (\$branch)" +} +PROMPT='%F{blue}%~%f%F{yellow}\$(parse_git_branch)%f +%F{green}>%f ' +OUTER + +# Install mise +if ! command -v mise >/dev/null 2>&1; then + echo "Installing mise..." + curl -fsSL https://mise.run | bash + export PATH="$HOME/.local/bin:$PATH" +else + echo "mise already installed: $(mise --version)" +fi + +cd /workspaces/arxiv-cli +mise trust +mise install +mise generate git-pre-commit + +# Install skill-bench +echo "Installing skill-bench..." +curl -fsSL https://raw.githubusercontent.com/sonesuke/skill-bench/main/scripts/setup.sh | sh + +# Configure arxiv-cli for Docker +mkdir -p "$HOME/.config/arxiv-cli" +cat > "$HOME/.config/arxiv-cli/config.toml" << 'EOF' +headless = true +browser_path = "/bin/chromium" +chrome_args = [ + "--no-sandbox", + "--disable-gpu" +] +EOF + +# Configure gh auth for git +if command -v gh >/dev/null 2>&1; then + echo "Configuring gh auth for git..." + gh auth setup-git +fi + +echo "Setup completed." diff --git a/scripts/up.sh b/scripts/up.sh new file mode 100755 index 0000000..9904f76 --- /dev/null +++ b/scripts/up.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +docker run -d \ + --name arxiv-cli \ + -v "$(pwd):/workspaces/arxiv-cli" \ + -v "${HOME}/.config/gh:/home/user/.config/gh" \ + -e Z_AI_API_KEY="${Z_AI_API_KEY}" \ + -e CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 \ + arxiv-cli:latest \ + sleep infinity