diff --git a/.agent/workspace/2026-02-06T00-00-00_ubuntu-custom-tools-discovery-guide.md b/.agent/workspace/2026-02-06T00-00-00_ubuntu-custom-tools-discovery-guide.md new file mode 100644 index 0000000..59bca50 --- /dev/null +++ b/.agent/workspace/2026-02-06T00-00-00_ubuntu-custom-tools-discovery-guide.md @@ -0,0 +1,747 @@ +# Discovering Custom Tools on Ubuntu: A Comprehensive Guide + +**Date:** 2026-02-06 +**Scope:** How to list all custom tools on Ubuntu and differentiate them from a stock Ubuntu image + +--- + +## Executive Summary + +Discovering all custom tools on an Ubuntu system requires checking multiple package managers, non-package installation methods, and custom installation locations. Tools can be installed via apt/dpkg, snap, flatpak, npm/yarn/pnpm/bun, cargo, pipx, and manual curl/wget scripts. To diff from stock Ubuntu, compare against the official package manifests published by Canonical. + +--- + +## 1. Package Manager-Based Tools + +### 1.1 APT/DPKG (Traditional Packages) + +These are the standard Ubuntu packages managed by the system package manager. + +```bash +# List all installed packages with details +dpkg -l + +# List only package names (cleaner output) +dpkg -l | grep '^ii' | awk '{print $2}' + +# Alternative using apt +apt list --installed + +# Count total installed packages +dpkg -l | grep '^ii' | wc -l + +# Export to file for comparison +dpkg -l > installed-packages.txt +``` + +**Key locations:** +- Binary files: `/usr/bin/`, `/bin/` +- Libraries: `/usr/lib/`, `/lib/` +- Documentation: `/usr/share/doc/` + +### 1.2 Snap Packages + +Snap is Ubuntu's universal packaging format with sandboxed applications. + +```bash +# List all installed snaps +snap list + +# List with more details +snap list --all + +# Show snap services +snap services + +# Export to file +snap list > installed-snaps.txt +``` + +**Key locations:** +- Snap files: `/snap/` or `/var/snap/` +- User data: `~/snap/` + +### 1.3 Flatpak Packages + +Flatpak provides cross-distribution sandboxed applications. + +```bash +# List all installed flatpaks +flatpak list + +# List by application ID only +flatpak list --app --columns=application + +# Show runtimes and apps +flatpak list --app --runtime + +# Export to file +flatpak list > installed-flatpaks.txt +``` + +**Key locations:** +- System apps: `/var/lib/flatpak/app/` +- User apps: `~/.local/share/flatpak/app/` + +--- + +## 2. Non-Package Manager Tools + +### 2.1 Node.js/NPM Global Packages + +Tools installed via npm/yarn/pnpm globally. + +```bash +# NPM global packages +npm list -g --depth=0 + +# NPM global packages with versions (full tree) +npm list -g + +# Get global prefix location +npm config get prefix + +# Yarn global packages (if installed) +yarn global list + +# Pnpm global packages (if installed) +pnpm list -g --depth=0 + +# Export NPM globals +npm list -g --depth=0 > npm-global-packages.txt +``` + +**Key locations:** +- Global binaries: `/usr/local/bin/` (npm) or `~/.local/bin/` +- Global node_modules: `/usr/local/lib/node_modules/` or `~/.local/lib/node_modules/` + +### 2.2 Bun Runtime Packages + +Bun is a fast JavaScript runtime and package manager (alternative to Node.js). + +```bash +# List local project packages (top-level only) +bun pm ls + +# List all packages including transitive dependencies +bun pm ls -a + +# List global packages (if bun supports it in your version) +bun pm ls -g 2>/dev/null || ls ~/.bun/install/global/node_modules/ + +# Show bun binary paths +bun pm bin # Local project bin +bun pm bin -g # Global bin directory + +# Show bun cache location +bun pm cache + +# Clear bun cache +bun pm cache rm + +# Check bun version and runtime info +bun --version +bun --revision + +# Export bun packages +bun pm ls > bun-packages.txt +``` + +**Key locations:** +- Bun binaries: `~/.bun/bin/` +- Global packages: `~/.bun/install/global/node_modules/` +- Global bin: `~/.bun/bin/` +- Cache: `~/.bun/install/cache/` +- Configuration: `bunfig.toml` (project) or `~/.bunfig.toml` (global) + +**Note:** As of early 2025, Bun's global package listing is still evolving. Check `~/.bun/install/global/node_modules/` directly or use `ls -la ~/.bun/bin/` for globally installed binaries. + +### 2.3 Rust/Cargo Installed Binaries + +Tools installed via `cargo install`. + +```bash +# List all installed cargo packages +# Shows package name, version, and associated binaries +cargo install --list + +# Alternative: list files in cargo bin directory +ls ~/.cargo/bin/ + +# Check cargo configuration +cat ~/.cargo/.crates.toml +cat ~/.cargo/.crates2.json | jq '.' + +# Third-party tool for updates (if installed) +cargo-binlist --list +cargo-list list + +# Export cargo packages +cargo install --list > cargo-packages.txt +``` + +**Key locations:** +- Binaries: `~/.cargo/bin/` (or `$CARGO_HOME/bin/`) +- Registry cache: `~/.cargo/registry/` + +### 2.3 Python/Pipx Installed Tools + +Tools installed via pipx (isolated Python applications). + +```bash +# List all pipx installed applications +pipx list + +# Short format (names only) +pipx list --short + +# JSON format for parsing +pipx list --json + +# List with injected packages +pipx list --include-injected + +# User pip packages (if using pip --user) +pip list --user + +# Show pipx environment +pipx environment + +# Export pipx packages +pipx list > pipx-packages.txt +``` + +**Key locations:** +- Virtual environments: `~/.local/share/pipx/venvs/` +- Exposed binaries: `~/.local/bin/` + +### 2.4 Go Installed Tools + +Tools installed via `go install`. + +```bash +# List installed go binaries +go list -m all 2>/dev/null || ls $(go env GOPATH)/bin/ + +# GOPATH bin location +echo $(go env GOPATH)/bin +ls $(go env GOPATH)/bin/ + +# GOBIN location (if set) +echo $GOBIN +ls $GOBIN 2>/dev/null || echo "GOBIN not set" +``` + +**Key locations:** +- Binaries: `$(go env GOPATH)/bin/` (default: `~/go/bin/`) +- Source: `$(go env GOPATH)/src/` + +### 2.5 .NET Global Tools + +.NET CLI tools installed via `dotnet tool install`. + +```bash +# List all installed .NET global tools +dotnet tool list -g + +# List local tools (in current directory) +dotnet tool list + +# Check if dotnet is installed and show version +dotnet --version +dotnet --info + +# List tool paths +echo "Global tools location:" +ls -la ~/.dotnet/tools/ + +# Export .NET tools +dotnet tool list -g > dotnet-tools.txt +``` + +**Key locations:** +- Global tools: `~/.dotnet/tools/` (or `$DOTNET_TOOLS/` if set) +- Local tools: `.config/dotnet-tools.json` (project-specific) +- Tool cache: `~/.nuget/packages/` +- Tool manifests: `~/.dotnet/toolResolverCache/` + +**Note:** .NET tools can be installed globally (available everywhere) or locally (project-specific). Use `-g` or `--global` flag for global installation. + +--- + +## 3. Manual/Script-Based Installations + +### 3.1 Common Custom Installation Directories + +Many tools install to these locations outside of package managers: + +```bash +# /usr/local (system-wide custom installs) +ls -la /usr/local/bin/ +ls -la /usr/local/lib/ +ls -la /usr/local/share/ +ls -la /usr/local/opt/ # Homebrew on Linux (Linuxbrew) + +# /opt (large third-party applications) +ls -la /opt/ + +# User-local installs +ls -la ~/.local/bin/ +ls -la ~/.local/lib/ +ls -la ~/.local/share/ + +# Home directories +ls -la ~/.bin/ 2>/dev/null || echo "~/.bin not found" +ls -la ~/bin/ 2>/dev/null || echo "~/bin not found" +ls -la ~/.dotfiles/bin/ 2>/dev/null || echo "~/.dotfiles/bin not found" + +# Application-specific +ls -la ~/.dotnet/tools/ 2>/dev/null || echo ".NET tools not found" +ls -la ~/.pulumi/bin/ 2>/dev/null || echo "Pulumi not found" +ls -la ~/.terraform.d/ 2>/dev/null || echo "Terraform plugins not found" +``` + +### 3.2 Shell Script Install Detection + +Find tools installed via curl/wget scripts: + +```bash +# Search for common installer patterns in command history +history | grep -E '(curl.*install|wget.*install|curl.*sh|wget.*sh)' | tail -20 + +# Check for install scripts in common locations +find ~ -maxdepth 2 -name "install*.sh" -o -name "setup*.sh" 2>/dev/null + +# Check /tmp for installer remnants +ls -la /tmp/ | grep -E '(install|setup|curl)' + +# Look at recently modified files in /usr/local +find /usr/local/bin -type f -mtime -30 2>/dev/null | head -20 +``` + +### 3.3 Container/Virtualization Tools + +```bash +# Docker images and tools +docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}" 2>/dev/null || echo "Docker not available" +docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null + +# Podman +podman images 2>/dev/null || echo "Podman not available" + +# Kubernetes tools +kubectl version --client 2>/dev/null || echo "kubectl not installed" +helm version --client 2>/dev/null || echo "Helm not installed" +minikube version 2>/dev/null || echo "Minikube not installed" +``` + +--- + +## 4. Environment and PATH Analysis + +### 4.1 Check PATH Components + +```bash +# Show current PATH +echo $PATH | tr ':' '\n' + +# Find all executables in PATH +# This can be slow on large systems +for dir in $(echo $PATH | tr ':' ' '); do + if [ -d "$dir" ]; then + echo "=== $dir ===" + ls "$dir" 2>/dev/null + fi +done + +# Quick count of executables in each PATH dir +for dir in $(echo $PATH | tr ':' ' '); do + if [ -d "$dir" ]; then + count=$(find "$dir" -maxdepth 1 -type f -executable 2>/dev/null | wc -l) + echo "$dir: $count executables" + fi +done +``` + +### 4.2 Shell-Specific Tools + +```bash +# Check shell rc files for tool installations +grep -E '(export PATH|alias|eval)' ~/.bashrc ~/.bash_profile ~/.zshrc ~/.zsh_profile 2>/dev/null | head -30 + +# Check for version managers +echo "=== Version Managers ===" +which rbenv rvm nvm n pyenv jenv 2>/dev/null + +# List available versions (if managers are installed) +rbenv versions 2>/dev/null || echo "rbenv not active" +nvm list 2>/dev/null || echo "nvm not active" +pyenv versions 2>/dev/null || echo "pyenv not active" +``` + +--- + +## 5. Diffing from Stock Ubuntu + +### 5.1 Get Stock Ubuntu Package Manifest + +Canonical publishes package manifests for each Ubuntu image: + +```bash +# Download manifest for your Ubuntu version +# Example for Ubuntu 22.04 LTS (Jammy) +UBUNTU_VERSION="jammy" +MANIFEST_URL="https://cloud-images.ubuntu.com/${UBUNTU_VERSION}/current/${UBUNTU_VERSION}-server-cloudimg-amd64.manifest" +wget -O stock-manifest.txt "$MANIFEST_URL" + +# Extract package names from manifest +grep -oP '^[a-z0-9\-\+\.]+' stock-manifest.txt | sort -u > stock-packages.txt + +# Get current installed packages +dpkg -l | grep '^ii' | awk '{print $2}' | sort -u > current-packages.txt + +# Find differences +# Packages not in stock (custom installed) +comm -23 current-packages.txt stock-packages.txt > custom-apt-packages.txt + +# Packages removed from stock +comm -13 current-packages.txt stock-packages.txt > removed-packages.txt +``` + +### 5.2 Automated System Comparison + +```bash +#!/bin/bash +# save-as: compare-to-stock.sh + +UBUNTU_VERSION=$(lsb_release -cs) +STOCK_URL="https://cloud-images.ubuntu.com/${UBUNTU_VERSION}/current/${UBUNTU_VERSION}-server-cloudimg-amd64.manifest" + +echo "Comparing to Ubuntu ${UBUNTU_VERSION} stock image..." + +# Download stock manifest +if ! wget -q -O /tmp/stock-manifest.txt "$STOCK_URL" 2>/dev/null; then + echo "Warning: Could not download manifest for ${UBUNTU_VERSION}" + echo "Trying to use local /var/lib/apt/extended_states..." + # Alternative: use apt-mark to find auto-installed packages + apt-mark showauto | sort -u > /tmp/stock-packages.txt +else + grep -oP '^[a-z0-9\-\+\.]+' /tmp/stock-manifest.txt | sort -u > /tmp/stock-packages.txt +fi + +# Current packages +dpkg -l | grep '^ii' | awk '{print $2}' | sort -u > /tmp/current-packages.txt + +echo "" +echo "=== CUSTOM APT PACKAGES (likely manually installed) ===" +comm -23 /tmp/current-packages.txt /tmp/stock-packages.txt + +echo "" +echo "=== CUSTOM SNAPS ===" +snap list | grep -v "^Name" + +echo "" +echo "=== CUSTOM FLATPAKS ===" +flatpak list --app --columns=application 2>/dev/null || echo "No flatpaks installed" + +echo "" +echo "=== /usr/local/bin CONTENTS ===" +ls -la /usr/local/bin/ 2>/dev/null | grep -v "^total" | tail -n +2 + +echo "" +echo "=== NPM GLOBALS ===" +npm list -g --depth=0 2>/dev/null || echo "No npm global packages" + +echo "" +echo "=== CARGO PACKAGES ===" +cargo install --list 2>/dev/null || echo "No cargo packages" + +echo "" +echo "=== PIPX PACKAGES ===" +pipx list --short 2>/dev/null || echo "No pipx packages" +``` + +### 5.3 Using diffoscope for Deep Comparison + +For comparing entire system states or container images: + +```bash +# Install diffoscope for deep comparison +sudo apt install diffoscope + +# Compare two package lists +diffoscope stock-packages.txt current-packages.txt --text diff.txt + +# Compare entire directories (useful for /usr/local) +diffoscope /reference/usr/local /usr/local --html diff-report.html +``` + +### 5.4 Using debsums for Integrity Check + +```bash +# Install debsums +sudo apt install debsums + +# Check for modified files in installed packages +sudo debsums -c + +# Generate manifest of all installed packages with checksums +sudo debsums -a > installed-package-checksums.txt +``` + +--- + +## 6. Complete System Inventory Script + +```bash +#!/bin/bash +# save-as: system-inventory.sh +# Generates a comprehensive report of all installed tools + +OUTPUT_DIR="$HOME/system-inventory-$(date +%Y%m%d)" +mkdir -p "$OUTPUT_DIR" + +echo "Generating system inventory in $OUTPUT_DIR..." + +# 1. APT/DPKG packages +echo "Collecting APT packages..." +dpkg -l > "$OUTPUT_DIR/apt-packages.txt" +apt list --installed > "$OUTPUT_DIR/apt-installed.txt" 2>/dev/null + +# 2. Snaps +echo "Collecting Snaps..." +snap list > "$OUTPUT_DIR/snaps.txt" 2>/dev/null || echo "No snaps installed" > "$OUTPUT_DIR/snaps.txt" + +# 3. Flatpaks +echo "Collecting Flatpaks..." +flatpak list > "$OUTPUT_DIR/flatpaks.txt" 2>/dev/null || echo "No flatpaks installed" > "$OUTPUT_DIR/flatpaks.txt" + +# 4. NPM globals +echo "Collecting NPM globals..." +npm list -g --depth=0 > "$OUTPUT_DIR/npm-globals.txt" 2>/dev/null || echo "npm not available" > "$OUTPUT_DIR/npm-globals.txt" + +# 5. Yarn globals +echo "Collecting Yarn globals..." +yarn global list > "$OUTPUT_DIR/yarn-globals.txt" 2>/dev/null || echo "yarn not available" > "$OUTPUT_DIR/yarn-globals.txt" + +# 6. Cargo packages +echo "Collecting Cargo packages..." +cargo install --list > "$OUTPUT_DIR/cargo-packages.txt" 2>/dev/null || echo "cargo not available" > "$OUTPUT_DIR/cargo-packages.txt" + +# 7. Pipx packages +echo "Collecting Pipx packages..." +pipx list > "$OUTPUT_DIR/pipx-packages.txt" 2>/dev/null || echo "pipx not available" > "$OUTPUT_DIR/pipx-packages.txt" + +# 8. User pip packages +echo "Collecting user pip packages..." +pip list --user > "$OUTPUT_DIR/pip-user-packages.txt" 2>/dev/null || pip3 list --user > "$OUTPUT_DIR/pip-user-packages.txt" 2>/dev/null || echo "pip not available" > "$OUTPUT_DIR/pip-user-packages.txt" + +# 9. Custom directories +echo "Collecting custom directory listings..." +ls -la /usr/local/bin/ > "$OUTPUT_DIR/usr-local-bin.txt" 2>/dev/null +ls -la /opt/ > "$OUTPUT_DIR/opt-contents.txt" 2>/dev/null +ls -la ~/.local/bin/ > "$OUTPUT_DIR/user-local-bin.txt" 2>/dev/null +ls -la ~/.cargo/bin/ > "$OUTPUT_DIR/cargo-bin.txt" 2>/dev/null +ls -la ~/.dotnet/tools/ > "$OUTPUT_DIR/dotnet-tools.txt" 2>/dev/null + +# 10. PATH analysis +echo "Collecting PATH info..." +echo "$PATH" | tr ':' '\n' > "$OUTPUT_DIR/path-dirs.txt" +for dir in $(echo $PATH | tr ':' ' '); do + if [ -d "$dir" ]; then + count=$(find "$dir" -maxdepth 1 -type f -executable 2>/dev/null | wc -l) + echo "$dir: $count executables" >> "$OUTPUT_DIR/path-executables.txt" + fi +done + +# 11. System info +echo "Collecting system info..." +lsb_release -a > "$OUTPUT_DIR/system-info.txt" 2>/dev/null +uname -a >> "$OUTPUT_DIR/system-info.txt" + +# 12. Summary report +echo "Creating summary report..." +cat > "$OUTPUT_DIR/INVENTORY-SUMMARY.txt" << 'EOF' +============================================= + SYSTEM INVENTORY REPORT +============================================= +Generated: $(date) + +CONTENTS: +--------- +1. apt-packages.txt - All dpkg/APT packages +2. apt-installed.txt - APT installed only +3. snaps.txt - Installed snap packages +4. flatpaks.txt - Installed flatpak packages +5. npm-globals.txt - NPM global packages +6. yarn-globals.txt - Yarn global packages +7. cargo-packages.txt - Cargo installed binaries +8. pipx-packages.txt - Pipx installed applications +9. pip-user-packages.txt - User Python packages + +DIRECTORIES SCANNED: +------------------- +10. usr-local-bin.txt - /usr/local/bin contents +11. opt-contents.txt - /opt/ contents +12. user-local-bin.txt - ~/.local/bin/ contents +13. cargo-bin.txt - ~/.cargo/bin/ contents +14. dotnet-tools.txt - ~/.dotnet/tools/ contents + +SYSTEM INFO: +----------- +15. path-dirs.txt - PATH directories +16. path-executables.txt - Executable counts per PATH dir +17. system-info.txt - OS and kernel information + +CUSTOM INSTALL DETECTION: +------------------------ +- Check /usr/local/bin/ for non-APT tools +- Check ~/.local/bin/ for user-installed tools +- Check ~/.cargo/bin/ for Rust tools +- Check ~/.dotnet/tools/ for .NET CLI tools +- Check /opt/ for large third-party apps + +TO DIFF FROM STOCK UBUNTU: +------------------------- +1. Download stock manifest: + wget https://cloud-images.ubuntu.com/$(lsb_release -cs)/current/$(lsb_release -cs)-server-cloudimg-amd64.manifest + +2. Extract package names: + grep -oP '^[a-z0-9\-\+\.]+' manifest | sort -u > stock.txt + +3. Compare with installed: + dpkg -l | grep '^ii' | awk '{print $2}' | sort -u > current.txt + comm -23 current.txt stock.txt > custom-packages.txt + +EOF + +echo "" +echo "=== INVENTORY COMPLETE ===" +echo "Location: $OUTPUT_DIR" +echo "" +echo "Quick Summary:" +echo "- APT packages: $(dpkg -l | grep '^ii' | wc -l)" +echo "- Snaps: $(snap list 2>/dev/null | tail -n +2 | wc -l)" +echo "- Flatpaks: $(flatpak list 2>/dev/null | wc -l)" +echo "- NPM globals: $(npm list -g --depth=0 2>/dev/null | tail -n +2 | wc -l)" +echo "- Cargo packages: $(cargo install --list 2>/dev/null | grep -c '^[a-z]' || echo 0)" +echo "- Pipx packages: $(pipx list --short 2>/dev/null | wc -l)" +echo "- /usr/local/bin: $(ls /usr/local/bin/ 2>/dev/null | wc -l)" +echo "- ~/.local/bin: $(ls ~/.local/bin/ 2>/dev/null | wc -l)" +echo "" +echo "See $OUTPUT_DIR/INVENTORY-SUMMARY.txt for details" +``` + +--- + +## 7. Identifying Specific Tool Categories + +### 7.1 Development Tools + +```bash +# IDEs and Editors +echo "=== IDEs/Editors ===" +which code vim nvim emacs nano pycharm idea rider webstorm 2>/dev/null + +# Version Control +echo "=== Version Control ===" +which git svn hg fossil 2>/dev/null +git --version 2>/dev/null + +# Build Tools +echo "=== Build Tools ===" +which make cmake ninja meson gradle mvn ant cargo 2>/dev/null + +# Compilers +echo "=== Compilers ===" +which gcc g++ clang rustc go javac python python3 node dotnet 2>/dev/null +``` + +### 7.2 CLI Productivity Tools + +```bash +# Search tools +echo "=== Search ===" +which fzf rg ag fd find 2>/dev/null + +# File managers +echo "=== File Managers ===" +which ranger nnn lf mc vifm 2>/dev/null + +# Terminal multiplexers +echo "=== Terminal Multiplexers ===" +which tmux screen byobu zellij 2>/dev/null + +# Text processing +echo "=== Text Processing ===" +which jq yq awk sed perl 2>/dev/null + +# Modern alternatives +echo "=== Modern CLI Tools ===" +which bat eza lsd delta dust duf procs sd choose 2>/dev/null +``` + +### 7.3 Cloud/DevOps Tools + +```bash +# Container tools +echo "=== Containers ===" +which docker podman nerdctl containerd 2>/dev/null +docker --version 2>/dev/null + +# Kubernetes +echo "=== Kubernetes ===" +which kubectl helm kustomize kubectx kubens k9s minikube kind k3s 2>/dev/null + +# Cloud CLIs +echo "=== Cloud CLIs ===" +which aws az gcloud oci doctl vultr scw 2>/dev/null + +# Infrastructure as Code +echo "=== IaC Tools ===" +which terraform pulumi ansible vagrant packer 2>/dev/null +``` + +--- + +## 8. Quick Reference Commands + +| Task | Command | +|------|---------| +| All APT packages | `dpkg -l` | +| All Snaps | `snap list` | +| All Flatpaks | `flatpak list` | +| NPM globals | `npm list -g --depth=0` | +| Yarn globals | `yarn global list` | +| Bun packages | `bun pm ls` | +| Cargo packages | `cargo install --list` | +| .NET global tools | `dotnet tool list -g` | +| Pipx packages | `pipx list --short` | +| User pip packages | `pip list --user` | +| /usr/local/bin | `ls -la /usr/local/bin/` | +| ~/.local/bin | `ls -la ~/.local/bin/` | +| ~/.cargo/bin | `ls -la ~/.cargo/bin/` | +| ~/.dotnet/tools | `ls -la ~/.dotnet/tools/` | +| ~/.bun/bin | `ls -la ~/.bun/bin/` | +| PATH executables | `find $(echo $PATH \| tr ':' ' ') -maxdepth 1 -type f -executable` | +| System services | `systemctl list-unit-files --state=enabled` | + +--- + +## 9. References + +- [Ubuntu Package Manifests](https://cloud-images.ubuntu.com/) - Official package lists for each Ubuntu release +- [diffoscope Documentation](https://diffoscope.org/) - Deep comparison tool +- [NPM Global Packages](https://docs.npmjs.com/cli/v8/commands/npm-list) - NPM listing docs +- [Bun Package Manager](https://bun.com/docs/pm/cli/pm) - Bun pm commands +- [Cargo Install](https://doc.rust-lang.org/cargo/commands/cargo-install.html) - Rust package management +- [.NET Global Tools](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) - .NET CLI tool management +- [Pipx Documentation](https://pipx.pypa.io/stable/) - Python application isolation +- [Snap Documentation](https://snapcraft.io/docs/command-reference) +- [Flatpak Documentation](https://docs.flatpak.org/en/latest/flatpak-command-reference.html) + +--- + +**End of Report** + +*Use the system-inventory.sh script to generate a complete snapshot of all installed tools on this Ubuntu system.* diff --git a/.agent/workspace/2026-02-23T00-00-00_code-review-release-readiness.md b/.agent/workspace/2026-02-23T00-00-00_code-review-release-readiness.md new file mode 100644 index 0000000..dc09294 --- /dev/null +++ b/.agent/workspace/2026-02-23T00-00-00_code-review-release-readiness.md @@ -0,0 +1,234 @@ +# TimeWarp.Builder — Code Review & Release Readiness Assessment + +**Date:** 2026-02-23 +**Reviewer:** claude-sonnet-4-6 (automated analysis) +**Branch:** Cramer-2025-12-22-dev +**Version:** 1.0.0-beta.1 + +--- + +## Executive Summary + +TimeWarp.Builder is a small, focused NuGet library providing two fluent builder interfaces (`IBuilder`, `INestedBuilder`) and four Kotlin-inspired scope extension methods. The code quality is **excellent** — clean, well-documented, AOT-compatible, and enforced by an aggressive analyzer stack. **The library is functionally ready for a v1.0.0 release**, but several issues must be resolved first: a stale repository reference in `msbuild/repository.props`, zero automated tests, and a thin README that leaves real consumers without enough guidance. + +--- + +## Scope + +This review covers: + +- All 4 C# source files in `source/timewarp-builder/` +- Project configuration: `timewarp-builder.csproj`, `Directory.Build.props`, `Directory.Packages.props` +- MSBuild props: `msbuild/repository.props` +- Developer tooling: `.editorconfig`, `.gitignore`, `timewarp-builder.slnx` +- Documentation: `README.md` +- Kanban board state +- Repo structure and packaging completeness + +--- + +## Methodology + +- Full read of every source file and configuration file in the repository +- Cross-reference of `msbuild/repository.props` against actual repo layout +- Analysis of NuGet packaging metadata in the `.csproj` +- Review of `.editorconfig` for consistency with `AGENTS.md` coding standards +- Review of `Directory.Packages.props` for unused/mismatched dependencies +- Comparison of README documentation against actual public API surface + +--- + +## Findings + +### 1. Source Code Quality — ✅ Excellent + +**Files reviewed:** `i-builder.cs`, `i-nested-builder.cs`, `scope-extensions.cs`, `global-usings.cs` + +All four source files are clean and correct. Specific observations: + +#### `i-builder.cs` +- Interface `IBuilder` uses `out` covariance correctly — this is the right choice for a producer-only interface. +- XML documentation is thorough: summary, ``, ``, and a working `` example. +- File-scoped namespace used correctly per project standards. +- No issues. + +#### `i-nested-builder.cs` +- `INestedBuilder where TParent : class` — the `class` constraint is correct and necessary; value types cannot be returned by reference in a builder chain. +- `out` covariance is appropriate. +- XML docs clearly explain the difference between this and `IBuilder` and include a realistic chained example. +- No issues. + +#### `scope-extensions.cs` +- All four methods (`Also`, `Apply`, `Let`, `Run`) are correctly implemented. +- `ArgumentNullException.ThrowIfNull(action)` is used everywhere — correct modern pattern. +- `Also` and `Apply` are **functionally identical** (both execute an `Action` and return the original object). This is documented as intentional ("semantically ... clearer intent for configuration"), which is acceptable for a public API. Consider whether consumers will find this intuitive or confusing long-term. +- Return types are consistent: `Also`/`Apply` return `T`, `Let` returns `TResult`, `Run` returns `void`. +- Generic constraints are minimal and correct — no unnecessary constraints. +- XML documentation is complete on all four methods. + +#### `global-usings.cs` +- Contains exactly one global using: `global using System;` +- `System` is needed for `Action`, `Func`, and `ArgumentNullException`. +- Minimal and correct per project standards. + +--- + +### 2. Project Configuration — ⚠️ One Blocker Found + +#### `timewarp-builder.csproj` +- `1.0.0-beta.1` — still on beta. This is what needs to be bumped for official release. +- `true`, `true`, `true` — all set correctly. +- `` and `` are present and sensible. +- `README.md` is included as a NuGet package README via `` — good. +- **Missing NuGet metadata:** Several standard fields are absent: + - `` — not set + - `` — not set + - `` — not set + - `` — not set (LICENSE file exists but is not referenced) + - `` — `assets/timewarp-builder-avatar.svg` exists but is not wired up + - These gaps affect discoverability and trustworthiness on NuGet.org + +#### `msbuild/repository.props` — 🔴 BLOCKER +```xml +timewarp-nuru +$(RepositoryRoot)timewarp-nuru.slnx +``` +These values reference **timewarp-nuru**, not **timewarp-builder**. This file was clearly copied from another repository and never updated. The actual solution file is `timewarp-builder.slnx`, not `timewarp-nuru.slnx`. + +While this does not break the build today (the `SolutionFile` property is defined but not consumed by any project), it is misleading, will cause confusion for contributors, and signals the repo was not fully set up from scratch. + +#### `Directory.Build.props` +- `TreatWarningsAsErrors`, `AnalysisMode=All`, `AnalysisLevel=latest-all` — excellent, high-quality defaults. +- AOT warning suppressions (`IL2026`, `IL2067`, etc.) are suppressed globally with a comment. These are listed as "not yet implemented" — this is acceptable for beta, but for a v1.0.0 release targeting AOT, these suppressions should ideally be resolved or scoped to specific files. +- `EmitCompilerGeneratedFiles=true` — good for debugging; no generated files currently exist for this library (no source generators in use). + +#### `Directory.Packages.props` +- Central Package Management is enabled — correct approach. +- **Contains many packages this library does not use** (Serilog, OpenTelemetry, Aspire, benchmarking frameworks, MCP, Mediator, etc.). These are almost certainly inherited from the timewarp-nuru monorepo and not relevant to this standalone library. While they don't cause a build problem (packages are only downloaded when referenced), they add noise and maintenance burden. +- `TimeWarp.Builder Version="1.0.0-beta.1"` references itself — this is only meaningful if some other project in the solution consumes the package. In the current single-project solution it is harmless but confusing. + +--- + +### 3. Documentation — ⚠️ Needs Improvement Before Release + +#### `README.md` +- Covers all 4 scope functions and both interfaces with code examples. +- **Does not include:** + - Installation instructions (`dotnet add package TimeWarp.Builder`) + - Target framework requirements (.NET 10 / .NET 5+?) + - License information + - A badge row (version, license, build status) — standard for NuGet libraries + - Any design rationale explaining when to choose `Also` vs `Apply` + - Link to NuGet.org or GitHub releases + - Contribution guidelines or link to them + +#### XML Documentation +- All public API surface is fully documented with ``, ``, ``, ``, ``, and `` — this is a strong point and will generate good IDE tooltips. + +--- + +### 4. Testing — 🔴 BLOCKER + +**There are zero tests in this repository.** No test project exists, and no test files were found. + +For a library of this size and simplicity, the risk of behavioral bugs is low. However: +- Without tests, there is no regression safety net. +- Publishing a v1.0.0 without any tests sets a poor precedent. +- The scope extension methods have subtle behavioral nuances (`Also` vs `Apply`, `null` guard on action, `Let` returning nullable results) that are worth specifying in tests. + +A minimal test suite covering the following would be sufficient: +- `Also` executes the action and returns the original object +- `Apply` executes the action and returns the original object +- `Let` transforms to a new type/value +- `Run` executes the action (terminal, no return) +- All four methods throw `ArgumentNullException` when `action`/`transform` is `null` +- `IBuilder` and `INestedBuilder` are implementable (integration-style tests using a concrete builder) + +--- + +### 5. API Design — ✅ Good, with Minor Considerations + +#### `Also` vs `Apply` semantic overlap +Both methods are identical in implementation. This is a documented design choice — `Also` is for side-effects (logging, debugging), `Apply` is for configuration. The distinction is useful but requires clear docs (currently present). Consider whether a single `Tap` method would serve the same purpose with less ambiguity, though changing this now would break the established naming from Kotlin conventions. + +#### `Run` terminal method +`Run` is void-returning and acts as a terminal operation. It is safe and well-designed. One edge case worth noting: if `obj` is `null`, the method still calls `action(obj)` which may throw a NullReferenceException inside the action. A guard for `null` obj could be considered on `Also`/`Apply`/`Let`/`Run`, but this would change the semantics for value types (structs) and nullable reference types, so the current behavior (no guard on `obj`) is likely correct. + +#### Covariance on interfaces +`IBuilder` and `INestedBuilder` both use `out` correctly, enabling useful polymorphic scenarios (e.g., `IBuilder` can hold a `IBuilder`). + +#### No async builder interface +There is no `IBuildAsync` or async variant of the scope extensions. Whether this is needed depends on consumer use cases. Not blocking for v1.0.0 but worth tracking for v1.1. + +--- + +### 6. Repo Housekeeping — ⚠️ Minor Issues + +| Item | Status | Notes | +|------|--------|-------| +| `.gitignore` | ✅ | Comprehensive Visual Studio standard gitignore | +| `LICENSE` | ✅ | File exists | +| `kanban/` | ✅ | All lanes empty — clean state | +| `assets/timewarp-builder-avatar.svg` | ⚠️ | Not referenced in `.csproj` as package icon | +| `msbuild/repository.props` | 🔴 | Wrong `RepositoryName` and `SolutionFile` (timewarp-nuru) | +| Solution file name | ✅ | `timewarp-builder.slnx` — correctly named | +| Single project in solution | ✅ | Clean, no extraneous projects | + +--- + +## Release Readiness Checklist + +### Blockers (must fix before v1.0.0) + +- [ ] **Fix `msbuild/repository.props`** — change `RepositoryName` to `timewarp-builder` and `SolutionFile` to `timewarp-builder.slnx` +- [ ] **Add automated tests** — even a minimal test project covering all public API methods +- [ ] **Add required NuGet metadata** — ``, ``, ``, `` to `timewarp-builder.csproj` +- [ ] **Bump version** — change `1.0.0-beta.1` to `1.0.0` in `timewarp-builder.csproj` + +### High Priority (strongly recommended before v1.0.0) + +- [ ] **Improve README** — add installation instructions, target framework info, license, and badges +- [ ] **Wire up package icon** — add `timewarp-builder-avatar.svg` and include the asset in the `.csproj` +- [ ] **Prune `Directory.Packages.props`** — remove packages not relevant to this library (Serilog, OpenTelemetry, benchmarking, MCP, etc.) +- [ ] **Resolve or scope AOT warning suppressions** — either implement AOT compatibility fully or add `#pragma warning disable` at call sites rather than globally suppressing in `Directory.Build.props` + +### Low Priority (nice to have for v1.x) + +- [ ] Add `#region Purpose` / `#region Design` context regions to source files (per project csharp skill conventions) +- [ ] Consider `IBuildAsync` interface for async build scenarios +- [ ] Consider adding `CHANGELOG.md` or release notes for v1.0.0 +- [ ] Consider XML doc comment suppression is turned off (`RCS1139`–`RCS1142`, `RCS1181` all set to `none`) — this means missing doc comments won't be flagged. Since all current public API is documented, this is fine now but could regress. + +--- + +## Positive Highlights + +These are genuinely strong practices that should be preserved: + +1. **Aggressive analyzer stack** — Roslynator + NetAnalyzers + CodeStyle analyzers with `TreatWarningsAsErrors=true` is exceptional. Very few .NET libraries enforce this level of quality at build time. +2. **Full XML documentation** — Every public method, parameter, type parameter, and exception is documented. Code examples are included and accurate. +3. **AOT flags set correctly** — `IsAotCompatible`, `EnableTrimAnalyzer`, `EnableAotAnalyzer` are all enabled. The actual code has no reflection or dynamic patterns, so AOT compatibility should be genuine. +4. **Covariance used correctly** — `out` on both generic interfaces is the right design. +5. **Null guards via `ArgumentNullException.ThrowIfNull`** — modern, concise, and consistent across all four extension methods. +6. **Central Package Management** — `ManagePackageVersionsCentrally=true` is the right approach for a multi-project repo. +7. **File naming convention** — kebab-case source file names (`i-builder.cs`, `scope-extensions.cs`) are consistent throughout. +8. **Minimal `global-usings.cs`** — only `System` is imported globally, keeping the namespace surface tight. + +--- + +## Summary Verdict + +| Category | Status | +|----------|--------| +| Code correctness | ✅ No bugs found | +| Code style/conventions | ✅ Fully compliant | +| API design | ✅ Clean and well-reasoned | +| AOT compatibility | ✅ Declared and structurally sound | +| Documentation (XML) | ✅ Complete | +| Documentation (README) | ⚠️ Thin — needs installation guide | +| Automated tests | 🔴 Missing entirely | +| NuGet packaging | ⚠️ Missing required metadata fields | +| Build configuration | ⚠️ `repository.props` has wrong repo name | +| Overall release readiness | **Not yet — 3 blockers to resolve** | + +The library itself is high quality. The blockers are all in tooling, packaging, and testing — not in the core logic. With focused effort on the four blockers above, this is ready for a clean v1.0.0 release. diff --git a/.editorconfig b/.editorconfig index a0bc851..5cc91d2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -26,7 +26,7 @@ insert_final_newline = true # ReSharper properties resharper_html_attribute_indent = single_indent -resharper_convert_to_primary_constructor_highlighting = false +resharper_convert_to_primary_constructor_highlighting = do_not_show # Development files [*.{cs,csx,cshtml,csproj,razor,sln,props,targets,json,yml,gitignore,}] @@ -142,7 +142,7 @@ csharp_prefer_braces = when-multiline:suggestion csharp_prefer_simple_using_statement = true:suggestion csharp_style_namespace_declarations = file_scoped:error csharp_style_prefer_method_group_conversion = true -csharp_style_prefer_primary_constructors = false +csharp_style_prefer_primary_constructors = false:none csharp_style_prefer_top_level_statements = false # Expression-level preferences diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..234188d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add bin diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..30f0f36 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,67 @@ +name: CI/CD Workflow + +on: + push: + branches: + - master + paths: + - 'source/**' + - 'tools/**' + - '.github/workflows/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'source/Directory.Build.props' + pull_request: + branches: + - master + paths: + - 'source/**' + - 'tools/**' + - '.github/workflows/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'source/Directory.Build.props' + release: + types: [published] + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for NuGet Trusted Publishing (OIDC) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for version detection + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: NuGet login (OIDC Trusted Publishing) + if: github.event_name == 'release' + id: nuget-login + uses: nuget/login@v1 + with: + user: TimeWarp.Enterprises + + - name: Run CI Pipeline + run: | + if [ "${{ github.event_name }}" == "release" ]; then + dotnet run tools/dev-cli/dev.cs -- workflow --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" + else + dotnet run tools/dev-cli/dev.cs -- workflow + fi + + - name: Upload Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: Packages-${{ github.run_number }} + path: artifacts/packages/*.nupkg + if-no-files-found: ignore diff --git a/.timewarp/dev.jsonc b/.timewarp/dev.jsonc new file mode 100644 index 0000000..858f817 --- /dev/null +++ b/.timewarp/dev.jsonc @@ -0,0 +1,6 @@ +{ + "checkVersionConfig": { + "checkVersionStrategy": "nuget-search", + "packages": "TimeWarp.Builder" + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e76fb83 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "peacock.remoteColor": "#F61D1F", + "timewarp.blurImagePath": "assets/timewarp-builder-avatar.svg", + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#f84e50", + "activityBar.background": "#f84e50", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#0cf50a", + "activityBarBadge.foreground": "#15202b", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#f84e50", + "statusBar.background": "#f61d1f", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#f84e50", + "statusBarItem.remoteBackground": "#f61d1f", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#f61d1f", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#f61d1f99", + "titleBar.inactiveForeground": "#e7e7e799" + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2ca21d2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# TimeWarp.Builder - Agent Development Guidelines + +## Build & Test Commands +```bash +dotnet build # Build all projects +dotnet build source/timewarp-builder/timewarp-builder.csproj # Build main project +dotnet pack # Create NuGet packages +dotnet test # Run all tests (if any test projects exist) +``` + +## Code Style Guidelines +- **Explicit types only**: Never use `var` - warnings enforced +- **Naming**: PascalCase for types, methods, properties; camelCase for parameters/locals; Interfaces start with 'I' +- **Fields**: PascalCase (NO underscore prefixes) +- **File-scoped namespaces**: `namespace TimeWarp.Builder;` +- **Using statements**: Inside namespace, not at file level +- **Nullability**: Enabled everywhere - use nullable reference types +- **AOT compatible**: All code must be AOT-compatible +- **Target framework**: .NET 10.0 + +## Project Structure +- Main library: `source/timewarp-builder/` +- Global usings in `global-usings.cs` (keep minimal) +- Fluent builder interfaces: `IBuilder` for standalone, `INestedBuilder` for nested +- Scope extensions for Kotlin-inspired chaining methods + +## Error Handling +- Use pattern matching (`is null` instead of `== null`) +- Prefer null propagation and coalescing expressions +- All warnings are treated as errors in build \ No newline at end of file diff --git a/BannedSymbols.txt b/BannedSymbols.txt new file mode 100644 index 0000000..167da45 --- /dev/null +++ b/BannedSymbols.txt @@ -0,0 +1 @@ +T:System.Console;Use TimeWarp.Terminal.Terminal static class or inject ITerminal instead diff --git a/Directory.Build.props b/Directory.Build.props index ef46975..e1d0149 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -51,7 +51,8 @@ - $(NoWarn);CA1014;CA1724;CA1812;IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053 + + $(NoWarn);CA1014;CA1724;CA1812;IDE0290;IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053 @@ -61,6 +62,12 @@ + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index c18d819..49dcafd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,89 +1,32 @@ + true - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + + - - + + \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..5668b38 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/timewarp-builder-avatar.svg b/assets/timewarp-builder-avatar.svg new file mode 100644 index 0000000..cdc0036 --- /dev/null +++ b/assets/timewarp-builder-avatar.svg @@ -0,0 +1,12 @@ + + + + timewarp-builder + \ No newline at end of file diff --git a/kanban/done/002-implement-dev-cli-for-timewarp-builder-repository.md b/kanban/done/002-implement-dev-cli-for-timewarp-builder-repository.md new file mode 100644 index 0000000..a22295e --- /dev/null +++ b/kanban/done/002-implement-dev-cli-for-timewarp-builder-repository.md @@ -0,0 +1,210 @@ +# Implement dev CLI for timewarp-builder repository + +## Description + +Create a TimeWarp.Nuru-based dev CLI tool for the timewarp-builder repository to provide consistent development commands. The dev CLI should follow the pattern used in other TimeWarp repositories and support common operations like build, test, clean, and pack. + +Reference implementation: See other TimeWarp repos (timewarp-nuru, timewarp-amuru) for examples of dev CLI structure. + +## Checklist + +### Project Setup + +- [x] **Create dev CLI project structure** + - Create `tools/dev-cli/` directory + - Create `tools/dev-cli/dev.cs` (entry point runfile) + - Create `tools/dev-cli/endpoints/` directory for command implementations + - Add dev CLI project to solution (`timewarp-builder.slnx`) + +- [x] **Add required package references** + - `TimeWarp.Nuru` (for CLI framework) + - `TimeWarp.Amuru` (for process execution) + - `TimeWarp.Terminal` (for console output) + - Reference the local `timewarp-builder` project if needed for testing + +### Core Commands + +- [x] **Implement `build` command** + - Build the main library: `dotnet build source/timewarp-builder/` + - Support `--configuration` option (Debug/Release) + - Support `--verbose` option + - Return appropriate exit codes + +- [x] **Implement `test` command** + - Run all tests: `dotnet test` (once test project exists) + - Support `--filter` option for selective test running + - Support `--no-build` option + - Show test results summary + +- [x] **Implement `clean` command** + - Clean build artifacts: `dotnet clean` + - Remove `artifacts/` directory contents + - Remove `bin/` and `obj/` folders from all projects + +- [x] **Implement `pack` command** + - Create NuGet package: `dotnet pack` + - Support `--configuration Release` default + - Support `--output` option for package location + - Output package to `artifacts/packages/` + +- [x] **Implement `self-install` command** + - AOT compile the dev CLI itself to `./bin/dev` + - Use `dotnet publish` with AOT flags + - Make executable: `chmod +x ./bin/dev` (on Unix) + - This enables fast subsequent runs without JIT overhead + +### Additional Commands (Optional but Recommended) + +- [ ] **Implement `restore` command** + - Restore NuGet packages: `dotnet restore` + - Verify Central Package Management is working + +- [x] **Implement `format` command** + - Run `dotnet format` to apply .editorconfig rules + - Support `--verify` option for CI (check-only, no changes) + +- [ ] **Implement `lint` command** + - Run analyzers and report warnings as errors + - Essentially `dotnet build` with strict settings + +- [x] **Implement `workflow` command** + - Run full CI/CD pipeline: clean -> build -> test (PR mode) + - Release mode: clean -> build -> check-version -> pack -> push + - Auto-detect mode from `GITHUB_EVENT_NAME` + +### Integration & Documentation + +- [x] **Wire up in solution** + - Add `` to `timewarp-builder.slnx` + - Or use folder structure if dev-cli is a folder-based runfile + +- [ ] **Update .gitignore** (leave unchecked — bin/ is already covered by existing .gitignore) + - Ensure `./bin/` is ignored (for self-installed dev CLI) + - Ensure `tools/dev-cli/bin/` and `obj/` are ignored + +- [ ] **Update AGENTS.md or create docs** (leave unchecked — not done yet) + - Document available dev CLI commands + - Add usage examples + - Reference: See other TimeWarp repo AGENTS.md files for patterns + +## Results + +**Commit:** e432432 (initial), subsequent commits for renaming +**Date:** 2026-02-23 + +### What was implemented +- `tools/dev-cli/dev.cs` — Entry point runfile using TimeWarp.Nuru Endpoint DSL +- `tools/dev-cli/Directory.Build.props` — Build config with AOT support, global usings, package references +- `tools/dev-cli/endpoints/build.cs` — Build library with `--configuration` and `--verbose` options +- `tools/dev-cli/endpoints/clean.cs` — Clean project + delete all bin/obj directories (targets .csproj not .slnx) +- `tools/dev-cli/endpoints/pack.cs` — Create NuGet packages to `artifacts/packages/` +- `tools/dev-cli/endpoints/test.cs` — Run test suite (gracefully handles missing tests directory) +- `tools/dev-cli/endpoints/format.cs` — Check/fix code formatting via `dotnet format` +- `tools/dev-cli/endpoints/self-install.cs` — AOT compile dev CLI to `./bin/dev` +- `tools/dev-cli/endpoints/workflow.cs` — Full CI/CD pipeline orchestration (ci command) +- `.envrc` — `PATH_add bin` for direnv integration +- `.github/workflows/workflow.yml` — GitHub Actions workflow calling `dev ci` + +### Files modified +- `Directory.Packages.props` — Added TimeWarp.Nuru 3.0.0-beta.54; updated TimeWarp.Amuru to 1.0.0-beta.20 +- `timewarp-builder.slnx` — Added `/tools/` solution folder with `tools/dev-cli/dev.cs` + +### Key decisions +- Used `Shell.Builder` fallback for `dotnet test` — `DotNet.Test()` does not exist in Amuru API +- Directory renamed from `commands/` to `endpoints/` to follow Nuru convention +- Files renamed from `*-command.cs` to `*.cs` (e.g., `build-command.cs` → `build.cs`) +- Renamed `Configuration` → `Config` in build/pack commands to avoid source-generator naming conflicts +- `clean.cs` targets `.csproj` directly instead of `.slnx` (the slnx parser fails on runfile entries) +- `test` command gracefully handles the case where `tests/` directory doesn't exist yet +- `.envrc` placed at repo root; `direnv allow` needed once after cloning + +### Test results +CLI compiles and `--help` shows all 7 commands: +``` +Commands: + build Build the TimeWarp.Builder library + ci Run full CI/CD pipeline + clean Clean solution and build artifacts + format Check or fix code formatting + pack Create NuGet packages + self-install AOT compile and install dev CLI to ./bin + test Run the test suite +``` + +`dev ci` runs successfully: +- Step 1/3 Clean ✅ +- Step 2/3 Build ✅ (0 warnings, 0 errors, package produced) +- Step 3/3 Test ✅ (gracefully reports no test project yet) + +## Notes + +### What is a Dev CLI? + +A dev CLI is a repository-specific command-line tool that provides standardized development tasks. Unlike generic `dotnet` commands, it: +- Encapsulates repository-specific knowledge (paths, configurations) +- Provides a consistent interface across all TimeWarp projects +- Can be AOT-compiled for fast execution via `self-install` +- Exposes `--capabilities` JSON for AI agent integration + +### Why TimeWarp.Nuru? + +TimeWarp.Nuru provides: +- Route-based command dispatch (like ASP.NET routing) +- Source generator for AOT compatibility +- Built-in help generation +- Consistent patterns across TimeWarp repos + +### Actual Structure in This Repo + +``` +tools/ + dev-cli/ + dev.cs # Entry point (runfile) + Directory.Build.props + endpoints/ # Named endpoints/ not commands/ + build.cs + clean.cs + format.cs + pack.cs + self-install.cs + test.cs + workflow.cs +``` + +### Commands Available + +| Command | Description | Example Usage | +|---------|-------------|---------------| +| `build` | Build the library | `./bin/dev build --configuration Release` | +| `test` | Run tests | `./bin/dev test --filter "AlsoTests"` | +| `clean` | Clean artifacts | `./bin/dev clean` | +| `pack` | Create NuGet package | `./bin/dev pack` | +| `format` | Format code | `./bin/dev format` | +| `self-install` | Install dev CLI | `dotnet run tools/dev-cli/dev.cs -- self-install` | +| `ci` / `workflow` | Run CI/CD pipeline | `dotnet run tools/dev-cli/dev.cs -- ci` | + +### AOT Considerations + +The dev CLI should be AOT-compatible: +- Use `TimeWarp.Nuru` source generators (already AOT-ready) +- Avoid reflection in command implementations +- Use `TimeWarp.Amuru` for process execution instead of `System.Diagnostics.Process` +- Test with `dotnet publish -p:PublishAot=true` + +### First-Time Setup + +```bash +# Initial setup (run once) +dotnet run tools/dev-cli/dev.cs -- self-install + +# Subsequent usage (fast AOT version) +./bin/dev build +./bin/dev test +./bin/dev ci +``` + +### Related Tasks + +- Task 001: Address code review blockers for v1.0.0 release + - Includes adding tests, which will run via `dev test` in CI + - The `dev ci` command already handles the case gracefully when no tests exist \ No newline at end of file diff --git a/kanban/done/003-create-github-workflow-for-cicd.md b/kanban/done/003-create-github-workflow-for-cicd.md new file mode 100644 index 0000000..522558a --- /dev/null +++ b/kanban/done/003-create-github-workflow-for-cicd.md @@ -0,0 +1,155 @@ +# Create GitHub workflow for CI/CD + +## Description + +Create a GitHub Actions workflow for continuous integration and NuGet package publishing, including implementing the `dev ci` command in the dev CLI. Reference implementations: +- GitHub workflow: `timewarp-terminal/.github/workflows/workflow.yml` +- CI command: `timewarp-nuru/master/tools/dev-cli/commands/ci-command.cs` + +## Results + +**Commit:** e494fad +**Date:** 2026-02-24 + +### What was implemented + +**`tools/dev-cli/endpoints/ci.cs`** +- `CiCommand` with `--mode` (pr/merge/release) and `--api-key` options +- Auto-detects mode from `GITHUB_EVENT_NAME` environment variable +- PR/Merge workflow: clean → build → test (3 steps) +- Release workflow: clean → build → check-version → pack → push (5 steps) +- `CheckVersionAsync`: reads version from `source/Directory.Build.props` via `XDocument`, checks NuGet.org for duplicate +- `PushPackagesAsync`: `dotnet nuget push` with optional OIDC API key +- `CiMode` enum (Pr, Merge, Release) at bottom of file + +**`.github/workflows/workflow.yml`** +- Triggers: push/PR to master (path filters), release published, workflow_dispatch +- OIDC Trusted Publishing via `nuget/login@v1` on release events only +- Calls `dotnet run tools/dev-cli/dev.cs -- ci` (with `--api-key` on release) +- Uploads `artifacts/packages/*.nupkg` artifacts always + +**`tools/dev-cli/endpoints/clean.cs` (fixed)** +- Changed target from `timewarp-builder.slnx` to `source/timewarp-builder/timewarp-builder.csproj` +- The `.slnx` parser cannot handle the `dev.cs` runfile entry in the solution + +### Test results + +`dotnet run tools/dev-cli/dev.cs -- ci` runs successfully: +- Step 1/3 Clean ✅ +- Step 2/3 Build ✅ (0 warnings, 0 errors, package produced) +- Step 3/3 Test ✅ (gracefully reports no test project yet) +- Pipeline SUCCEEDED + +## Checklist + +### Dev CLI - Implement CI Command + +- [x] **Create `tools/dev-cli/endpoints/ci.cs`** + - Reference: `timewarp-nuru/master/tools/dev-cli/commands/ci-command.cs` + - Route: `[NuruRoute("ci", Description = "Run full CI/CD pipeline")]` + - Options: `--mode` (pr/merge/release), `--api-key` (for NuGet publishing) + - Auto-detect mode from `GITHUB_EVENT_NAME` environment variable + - Implement `CiMode` enum: Pr, Merge, Release + +- [x] **Implement PR/merge workflow** + - Steps: clean -> build -> test (skip verify-samples - not applicable) + - Run each step by calling other command handlers directly (like the reference does) + +- [x] **Implement release workflow** + - Steps: clean -> build -> check-version -> pack -> push + - Check-version: verify version hasn't been published to NuGet.org + - Pack: create NuGet packages via `dotnet pack` + - Push: push packages to NuGet.org via `dotnet nuget push` + +- [x] **Test the CI command locally** + - `dotnet run tools/dev-cli/dev.cs -- ci` — should run default PR workflow + - `dotnet run tools/dev-cli/dev.cs -- ci --mode release` — should run release workflow + +### GitHub Workflow - Create workflow.yml + +- [x] **Create `.github/workflows/` directory structure** + - Path: `.github/workflows/` + - Main workflow file: `.github/workflows/workflow.yml` + +- [x] **Create workflow file with CI pipeline** + - Trigger on: push to master, PRs to master, release published, manual dispatch + - Path filters: `source/**`, `tools/**`, `.github/workflows/**`, `Directory.Build.props`, `Directory.Packages.props`, `source/Directory.Build.props` + - Jobs: `ci` job on `ubuntu-latest` + +- [x] **Add checkout and .NET setup steps** + - `actions/checkout@v4` with `fetch-depth: 0` (for version detection) + - `actions/setup-dotnet@v4` with `dotnet-version: '10.0.x'` + +- [x] **Add dev CLI ci command** + - `dotnet run tools/dev-cli/dev.cs -- ci` + - Pass `--api-key` on release events for NuGet publishing + +- [x] **Add artifact upload step** + - Upload `artifacts/packages/*.nupkg` on failure/always + - Use `actions/upload-artifact@v4` + +### Release Publishing (Optional for v1.0.0) + +- [x] **Add NuGet Trusted Publishing (OIDC)** + - `nuget/login@v1` action on release events + - Requires NuGet.org publisher configuration + - Requires `id-token: write` permission + +- [x] **Wire up release workflow command** + - Pass `--api-key` on release events for NuGet publishing + +### Repository Configuration + +- [ ] **Verify branch protection rules** + - Require PR reviews before merge to master + - Require CI checks to pass + +## Notes + +### Reference: timewarp-nuru ci-command.cs + +The reference implementation at `timewarp-nuru/master/tools/dev-cli/commands/ci-command.cs` shows: + +```csharp +[NuruRoute("ci", Description = "Run full CI/CD pipeline")] +internal sealed class CiCommand : ICommand +{ + [Option("mode", "m", Description = "CI mode: pr, merge, or release")] + public string? Mode { get; set; } + + [Option("api-key", Description = "NuGet API key for publishing")] + public string? ApiKey { get; set; } + + // Determines mode from GITHUB_EVENT_NAME or explicit --mode flag + // Pr: clean -> build -> verify-samples -> test + // Release: clean -> build -> check-version -> pack -> push + + // Calls other command handlers directly: + CleanCommand.Handler cleanHandler = new(Terminal); + await cleanHandler.Handle(new CleanCommand(), CancellationToken.None); +} +``` + +### Key simplifications for timewarp-builder + +- **No verify-samples step** — this library has no samples +- **Simpler test step** — just `dotnet test` (test project doesn't exist yet, so workflow will need updating once it does) +- **Single project** — only `source/timewarp-builder/timewarp-builder.csproj` to build/pack + +### Files to Create/Modify + +**New files:** +- `.github/workflows/workflow.yml` +- `tools/dev-cli/endpoints/ci.cs` + +**Reference for ci.cs structure:** +- Use `ITerminal` for output (injected via constructor) +- Use `Shell.Builder` for running dotnet commands +- Read version from `source/Directory.Build.props` using `XDocument` +- Push to `https://api.nuget.org/v3/index.json` + +### Related Tasks + +- Task 001: Address code review blockers for v1.0.0 release + - Includes adding tests, which will need to run in the CI workflow + - The `test` step in CI will work once tests are added \ No newline at end of file diff --git a/kanban/done/004-add-bannedapianalyzers-to-timewarp-builder.md b/kanban/done/004-add-bannedapianalyzers-to-timewarp-builder.md new file mode 100644 index 0000000..24d8dd2 --- /dev/null +++ b/kanban/done/004-add-bannedapianalyzers-to-timewarp-builder.md @@ -0,0 +1,27 @@ +# Add BannedApiAnalyzers to timewarp-builder + +## Description + +Add BannedApiAnalyzers to enforce banned API patterns. Reference: timewarp-ganda project configuration. + +## Checklist + +- [x] Add `Microsoft.CodeAnalysis.BannedApiAnalyzers` package reference to `Directory.Build.props` +- [x] Add `AdditionalFiles` ItemGroup for `BannedSymbols.txt` in `Directory.Build.props` +- [x] Create `BannedSymbols.txt` file at repository root with banned API definitions +- [x] Build project to verify analyzer works correctly + +## Notes + +Reference from timewarp-ganda: +- Package: `Microsoft.CodeAnalysis.BannedApiAnalyzers` +- BannedSymbols.txt location: `$(MSBuildThisFileDirectory)BannedSymbols.txt` +- Initially ban `System.Console` - use `TimeWarp.Terminal.Terminal` or inject `ITerminal` instead + +## Results + +- Added `Microsoft.CodeAnalysis.BannedApiAnalyzers` v3.3.4 to `Directory.Packages.props` +- Added `PackageReference` with `PrivateAssets="all"` to `Directory.Build.props` Code Analyzers ItemGroup +- Added `ItemGroup Label="Banned API Files"` with `AdditionalFiles` pointing to `BannedSymbols.txt` in `Directory.Build.props` +- Created `BannedSymbols.txt` at repo root banning `T:System.Console` with message directing to `TimeWarp.Terminal.Terminal` or `ITerminal` +- Build succeeded with 0 warnings, 0 errors diff --git a/kanban/in-progress/001-address-code-review-blockers-for-v100-release.md b/kanban/in-progress/001-address-code-review-blockers-for-v100-release.md new file mode 100644 index 0000000..1a60da4 --- /dev/null +++ b/kanban/in-progress/001-address-code-review-blockers-for-v100-release.md @@ -0,0 +1,118 @@ +# Address code review blockers for v1.0.0 release + +## Description + +Resolve all blockers and high-priority items identified in the comprehensive code review to prepare TimeWarp.Builder for an official v1.0.0 release. + +Code review report: `.agent/workspace/2026-02-23T00-00-00_code-review-release-readiness.md` + +## Checklist + +### 🔴 Blockers (must fix before v1.0.0) + +- [x] **Fix `msbuild/repository.props`** (commit: c5ab2b6) + - Change `RepositoryName` from `timewarp-nuru` to `timewarp-builder` + - Change `SolutionFile` from `timewarp-nuru.slnx` to `timewarp-builder.slnx` + - Add `ToolsDirectory` for future tooling support + - Note: TestsDirectory, SamplesDirectory, BenchmarksDirectory left in place for future use + +- [ ] **Add automated tests** + - Create test project (suggested: `tests/TimeWarp.Builder.Tests/`) + - Test all 4 scope extension methods: + - `Also` - executes action, returns original object + - `Apply` - executes action, returns original object + - `Let` - transforms to new type/value + - `Run` - executes action (terminal, void) + - Test null guard behavior - all methods throw `ArgumentNullException` when callback is null + - Test interface implementations work correctly (integration-style with concrete builders) + +- [x] **Add required NuGet metadata** (commit: 2e47e84) + - Metadata is in `source/Directory.Build.props` (shared by all source projects) + - `Steven T. Cramer` + - `https://github.com/TimeWarpEngineering/timewarp-builder` + - `Unlicense` (matches LICENSE file) + +- [x] **Bump version for release** (commit: 49848cd) + - Changed `1.0.0-beta.1` to `1.0.0-beta.2` in `source/Directory.Build.props` + +### ⚠️ High Priority (strongly recommended) + +- [ ] **Improve README.md** + - Add installation section with `dotnet add package TimeWarp.Builder` + - Add requirements section (target framework: .NET 10.0) + - Add license badge and link to LICENSE file + - Add NuGet version badge + - Add design rationale explaining `Also` vs `Apply` distinction + - Link to GitHub repository + +- [ ] **Wire up package icon** + - Add `timewarp-builder-avatar.png` (convert SVG to PNG if needed, NuGet prefers PNG) + - Add icon file reference to `.csproj`: + ```xml + + + + ``` + - Note: `assets/timewarp-builder-avatar.svg` exists but SVG icons in NuGet packages have limited client support + +- [ ] **Prune `Directory.Packages.props`** + - Remove unused package groups: + - Serilog (Logging - Serilog section) + - OpenTelemetry + - Aspire + - Benchmark - CLI Frameworks + - Benchmark - Performance + - MCP Server + - Mediator + - Keep only packages actually referenced by this library (likely just analyzers and Microsoft.Extensions if needed) + - Note: `TimeWarp.Builder` self-reference can also be removed + +- [ ] **Review AOT warning suppressions** + - Current global suppressions in `Directory.Build.props`: `IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053` + - Evaluate if these are truly needed for this library (it has no reflection/dynamic code) + - If they are false positives, consider removing the global suppression and testing AOT build + +### Nice to Have (low priority) + +- [ ] Add `#region Purpose` / `#region Design` context blocks to source files per csharp skill conventions +- [ ] Add `CHANGELOG.md` for v1.0.0 release notes +- [ ] Consider `IBuildAsync` interface for async build scenarios (future v1.x) + +## Notes + +### Code Review Summary + +**Reviewer:** claude-sonnet-4-6 (automated analysis) +**Date:** 2026-02-23 +**Branch:** Cramer-2025-12-22-dev +**Version:** 1.0.0-beta.2 + +**Overall Assessment:** The C# source code is excellent - clean, well-documented, AOT-compatible, with aggressive analyzer enforcement. All 4 files are correct and fully documented. The blockers are all in tooling, packaging, and testing infrastructure - not in the core logic. + +**Positive Highlights:** +- Aggressive analyzer stack (Roslynator + NetAnalyzers + TreatWarningsAsErrors) +- Full XML documentation with code examples +- Correct use of covariance on interfaces +- Modern null guards via `ArgumentNullException.ThrowIfNull` +- AOT compatibility flags correctly set +- Central Package Management enabled +- Clean file naming conventions + +**Key Issues Found:** +1. ~~`msbuild/repository.props` has stale references to `timewarp-nuru`~~ - FIXED +2. Zero automated tests - no test project exists +3. ~~Missing required NuGet metadata (Authors, URLs, License)~~ - FIXED (in source/Directory.Build.props) +4. README lacks installation instructions and badges +5. Package icon exists but isn't referenced in `.csproj` (currently uses logo.png) +6. `Directory.Packages.props` contains unused packages from monorepo + +With the blockers resolved, this library is ready for a clean v1.0.0 release. + +### Files Involved +- `msbuild/repository.props` - fix stale references +- `source/timewarp-builder/timewarp-builder.csproj` - add metadata, bump version +- `Directory.Packages.props` - remove unused packages +- `Directory.Build.props` - review AOT suppressions +- `README.md` - add installation, badges, requirements +- New: `tests/TimeWarp.Builder.Tests/` - create test project +- `assets/timewarp-builder-avatar.svg` - convert to PNG and wire up diff --git a/kanban/overview.md b/kanban/overview.md new file mode 100644 index 0000000..c851f0c --- /dev/null +++ b/kanban/overview.md @@ -0,0 +1,38 @@ +# Kanban Board Overview + +This is a five-state kanban system for managing tasks: + +- **backlog/** - Tasks planned but not yet ready to work on +- **to-do/** - Planned tasks ready to be started +- **in-progress/** - Tasks currently being worked on +- **done/** - Completed tasks +- **archived/** - Cancelled, obsolete, or indefinitely deferred tasks + +## Task Naming + +- Simple tasks: `NNN-task-description.md` +- Complex tasks: `NNN-task-description/task.md` (folder structure) +- All tasks use three-digit numbering (001-999) + +## Usage + +```bash +kanban # Show board (To-Do and In-Progress only) +kanban --show-done # Show board including Done column +kanban --show-backlog # Show board including Backlog column +kanban --show-archived # Show board including Archived column +kanban create "Task title" # Create task in to-do +kanban move # Move task to different column +kanban done # Move task to done +kanban archive # Archive task +kanban show # Show task details +``` + +## When to use archived/ vs done/ + +- **done/** - Task was completed successfully +- **archived/** - Task was NOT completed but is no longer active: + - Cancelled (decided not to do it) + - Obsolete (no longer relevant due to other changes) + - Superseded (replaced by a different approach) + - Indefinitely deferred (might revive later, but not actively planned) \ No newline at end of file diff --git a/kanban/task-template.md b/kanban/task-template.md new file mode 100644 index 0000000..5373ed9 --- /dev/null +++ b/kanban/task-template.md @@ -0,0 +1,19 @@ +# Task Template + +## Summary + +[Brief description of the task - 1-2 sentences] + +## Todo List + +- [ ] Task item 1 +- [ ] Task item 2 +- [ ] Task item 3 + +## Notes + +[Context, implementation details, decisions, and technical information that should be preserved for future reference] + +## Results + +[Added after completion - outcomes, metrics, observations, and decisions made] \ No newline at end of file diff --git a/msbuild/repository.props b/msbuild/repository.props index 69cadab..15151c0 100644 --- a/msbuild/repository.props +++ b/msbuild/repository.props @@ -1,14 +1,14 @@ - timewarp-nuru + timewarp-builder $(MSBuildThisFileDirectory)../ - $(RepositoryRoot)timewarp-nuru.slnx + $(RepositoryRoot)timewarp-builder.slnx $(RepositoryRoot)source/ $(RepositoryRoot)tests/ $(RepositoryRoot)samples/ $(RepositoryRoot)benchmarks/ - $(RepositoryRoot)scripts/ + $(RepositoryRoot)tools/ $(RepositoryRoot)artifacts/ $(ArtifactsDirectory)packages/ diff --git a/source/timewarp-builder/readme.md b/readme.md similarity index 100% rename from source/timewarp-builder/readme.md rename to readme.md diff --git a/source/Directory.Build.props b/source/Directory.Build.props new file mode 100644 index 0000000..85fb734 --- /dev/null +++ b/source/Directory.Build.props @@ -0,0 +1,36 @@ + + + + + + + 1.0.0-beta.2 + Steven T. Cramer + https://github.com/TimeWarpEngineering/timewarp-builder + Unlicense + logo.png + readme.md + + + + true + $(ArtifactsDirectory)packages/ + true + + + + true + true + false + embedded + + + + + + + + + + + diff --git a/source/timewarp-builder/timewarp-builder.csproj b/source/timewarp-builder/timewarp-builder.csproj index 23dff81..9f63cb4 100644 --- a/source/timewarp-builder/timewarp-builder.csproj +++ b/source/timewarp-builder/timewarp-builder.csproj @@ -1,8 +1,6 @@ - - 1.0.0-beta.1 TimeWarp.Builder TimeWarp.Builder Fluent builder interfaces and scope extensions for TimeWarp projects @@ -15,7 +13,7 @@ - + diff --git a/timewarp-builder.slnx b/timewarp-builder.slnx index 0a91e6a..9c18b9c 100644 --- a/timewarp-builder.slnx +++ b/timewarp-builder.slnx @@ -2,4 +2,7 @@ + + + diff --git a/tools/dev-cli/Directory.Build.props b/tools/dev-cli/Directory.Build.props new file mode 100644 index 0000000..57c3b15 --- /dev/null +++ b/tools/dev-cli/Directory.Build.props @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + $(NoWarn);CA1031;CA1303;CA1508;CA1515;CA1849;CA2007;CA2016;CA2000;RCS1046 + + + true + true + + + $(InterceptorsNamespaces);TimeWarp.Nuru.Generated + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/dev-cli/dev.cs b/tools/dev-cli/dev.cs new file mode 100755 index 0000000..b045f27 --- /dev/null +++ b/tools/dev-cli/dev.cs @@ -0,0 +1,34 @@ +#!/usr/bin/dotnet -- +// ═══════════════════════════════════════════════════════════════════════════════ +// DEV CLI - TIMEWARP.BUILDER DEVELOPMENT TOOL +// ═══════════════════════════════════════════════════════════════════════════════ +// +// This is the development CLI for TimeWarp.Builder that provides: +// - Build, test, pack, and clean commands +// - AOT-compiled binary for fast execution +// +// Usage: +// As runfile: dotnet tools/dev-cli/dev.cs +// As AOT: ./bin/dev +// +// Commands: +// dev build - Build the TimeWarp.Builder library +// dev test - Run tests +// dev clean - Clean solution and artifacts +// dev pack - Create NuGet packages +// dev format - Check/fix code formatting +// dev self-install - AOT compile and install dev CLI to ./bin +// +// To bootstrap: +// dotnet run tools/dev-cli/dev.cs -- self-install +// direnv allow +// dev --help +// ═══════════════════════════════════════════════════════════════════════════════ + +NuruApp app = NuruApp.CreateBuilder() + .WithName("dev") + .WithDescription("Development CLI for TimeWarp.Builder") + .DiscoverEndpoints() + .Build(); + +return await app.RunAsync(args); diff --git a/tools/dev-cli/endpoints/.editorconfig b/tools/dev-cli/endpoints/.editorconfig new file mode 100644 index 0000000..e69de29 diff --git a/tools/dev-cli/endpoints/GlobalSuppressions.cs b/tools/dev-cli/endpoints/GlobalSuppressions.cs new file mode 100644 index 0000000..e3c6ba3 --- /dev/null +++ b/tools/dev-cli/endpoints/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "", Scope = "member", Target = "~M:DevCli.Commands.BuildCommand.Handler.#ctor(ITerminal)")] diff --git a/tools/dev-cli/endpoints/build.cs b/tools/dev-cli/endpoints/build.cs new file mode 100644 index 0000000..3418724 --- /dev/null +++ b/tools/dev-cli/endpoints/build.cs @@ -0,0 +1,63 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// BUILD COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Builds the TimeWarp.Builder library. + +namespace DevCli.Commands; + +/// +/// Build the TimeWarp.Builder library. +/// +[NuruRoute("build", Description = "Build the TimeWarp.Builder library")] +internal sealed class BuildCommand : ICommand +{ + [Option("configuration", "c", Description = "Build configuration (Debug or Release)")] + public string Config { get; set; } = "Release"; + + [Option("verbose", "v", Description = "Verbose output")] + public bool Verbose { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(BuildCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string projectPath = Path.Combine(repoRoot, "source", "timewarp-builder", "timewarp-builder.csproj"); + string verbosity = command.Verbose ? "normal" : "minimal"; + + Terminal.WriteLine("Building TimeWarp.Builder..."); + Terminal.WriteLine($"Configuration: {command.Config}"); + Terminal.WriteLine($"Working from: {repoRoot}"); + + int exitCode = await DotNet.Build() + .WithProject(projectPath) + .WithConfiguration(command.Config) + .WithVerbosity(verbosity) + .RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + Terminal.WriteErrorLine("Build failed!"); + return Unit.Value; + } + + Terminal.WriteLine("\n✅ Build completed successfully!"); + return Unit.Value; + } + } +} diff --git a/tools/dev-cli/endpoints/clean.cs b/tools/dev-cli/endpoints/clean.cs new file mode 100644 index 0000000..c58f766 --- /dev/null +++ b/tools/dev-cli/endpoints/clean.cs @@ -0,0 +1,76 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// CLEAN COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Cleans the TimeWarp.Builder project and deletes all bin/obj directories. + +namespace DevCli.Commands; + +/// +/// Clean the project and all build artifacts. +/// +[NuruRoute("clean", Description = "Clean solution and build artifacts")] +internal sealed class CleanCommand : ICommand +{ + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(CleanCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string projectPath = Path.Combine(repoRoot, "source", "timewarp-builder", "timewarp-builder.csproj"); + + Terminal.WriteLine("Cleaning TimeWarp.Builder..."); + Terminal.WriteLine($"Working from: {repoRoot}"); + + int exitCode = await DotNet.Clean() + .WithProject(projectPath) + .WithVerbosity("minimal") + .RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + Terminal.WriteErrorLine("dotnet clean failed!"); + return Value; + } + + // Also delete obj and bin directories for a thorough clean + Terminal.WriteLine("\nDeleting obj and bin directories..."); + string[] directoriesToDelete = + [ + .. Directory.GetDirectories(repoRoot, "obj", SearchOption.AllDirectories) + .Concat(Directory.GetDirectories(repoRoot, "bin", SearchOption.AllDirectories)) + .Where(d => !d.Contains(Path.Combine("tools", "dev-cli"))), + ]; + + foreach (string dir in directoriesToDelete) + { + try + { + Directory.Delete(dir, recursive: true); + Terminal.WriteLine($" Deleted: {dir}"); + } + catch (Exception ex) + { + Terminal.WriteLine($" Warning: Could not delete {dir}: {ex.Message}"); + } + } + + Terminal.WriteLine("\n✅ Clean completed successfully!"); + return Value; + } + } +} diff --git a/tools/dev-cli/endpoints/format.cs b/tools/dev-cli/endpoints/format.cs new file mode 100644 index 0000000..32c95b1 --- /dev/null +++ b/tools/dev-cli/endpoints/format.cs @@ -0,0 +1,73 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// FORMAT COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Check or fix code formatting using dotnet format. + +namespace DevCli.Commands; + +/// +/// Check or fix code formatting. +/// +[NuruRoute("format", Description = "Check or fix code formatting")] +internal sealed class FormatCommand : ICommand +{ + [Option("fix", "f", Description = "Fix formatting issues instead of just checking")] + public bool Fix { get; set; } + + [Option("verbose", "v", Description = "Verbose output")] + public bool Verbose { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(FormatCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot(); + + if (repoRoot is null) + { + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + } + + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string solutionPath = Path.Combine(repoRoot, "timewarp-builder.slnx"); + + Terminal.WriteLine(command.Fix ? "Fixing code formatting..." : "Checking code formatting..."); + + List formatArgs = ["format", solutionPath, "--severity", "warn"]; + + if (!command.Fix) + { + formatArgs.Add("--verify-no-changes"); + } + + int exitCode = await Shell.Builder("dotnet") + .WithArguments([.. formatArgs]) + .WithNoValidation() + .RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + string message = command.Fix + ? "Format failed!" + : "Code style violations found! Run 'dev format --fix' to fix them."; + Terminal.WriteErrorLine(message); + return Unit.Value; + } + + Terminal.WriteLine(command.Fix ? "✅ Formatting fixed!" : "✅ Code style check passed!"); + return Unit.Value; + } + } +} diff --git a/tools/dev-cli/endpoints/pack.cs b/tools/dev-cli/endpoints/pack.cs new file mode 100644 index 0000000..99ac37d --- /dev/null +++ b/tools/dev-cli/endpoints/pack.cs @@ -0,0 +1,75 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// PACK COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Creates NuGet packages for the TimeWarp.Builder library. + +namespace DevCli.Commands; + +/// +/// Create NuGet packages for TimeWarp.Builder. +/// +[NuruRoute("pack", Description = "Create NuGet packages")] +internal sealed class PackCommand : ICommand +{ + [Option("configuration", "c", Description = "Build configuration (Debug or Release)")] + public string Config { get; set; } = "Release"; + + [Option("verbose", "v", Description = "Verbose output")] + public bool Verbose { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(PackCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string projectPath = Path.Combine(repoRoot, "source", "timewarp-builder", "timewarp-builder.csproj"); + string outputPath = Path.Combine(repoRoot, "artifacts", "packages"); + string verbosity = command.Verbose ? "normal" : "minimal"; + + Terminal.WriteLine("Creating NuGet packages for TimeWarp.Builder..."); + Terminal.WriteLine($"Configuration: {command.Config}"); + Terminal.WriteLine($"Output: {outputPath}"); + + Directory.CreateDirectory(outputPath); + + int exitCode = await DotNet.Pack() + .WithProject(projectPath) + .WithConfiguration(command.Config) + .WithOutput(outputPath) + .WithVerbosity(verbosity) + .RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + Terminal.WriteErrorLine("Pack failed!"); + return Value; + } + + // List produced packages + string[] packages = Directory.GetFiles(outputPath, "*.nupkg"); + Terminal.WriteLine("\n✅ Pack completed successfully!"); + Terminal.WriteLine($"Packages in {outputPath}:"); + foreach (string package in packages) + { + Terminal.WriteLine($" {Path.GetFileName(package)}"); + } + + return Value; + } + } +} diff --git a/tools/dev-cli/endpoints/self-install.cs b/tools/dev-cli/endpoints/self-install.cs new file mode 100644 index 0000000..0d44b65 --- /dev/null +++ b/tools/dev-cli/endpoints/self-install.cs @@ -0,0 +1,100 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// SELF-INSTALL COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// AOT compiles and installs the dev CLI to ./bin for fast execution via direnv PATH. + +namespace DevCli.Commands; + +/// +/// AOT compile and install dev CLI to ./bin directory. +/// +[NuruRoute("self-install", Description = "AOT compile and install dev CLI to ./bin")] +internal sealed class SelfInstallCommand : ICommand +{ + [Option("verbose", "v", Description = "Verbose output")] + public bool Verbose { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(SelfInstallCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string devCliSource = Path.Combine(repoRoot, "tools", "dev-cli", "dev.cs"); + string outputPath = Path.Combine(repoRoot, "bin"); + string rid = GetRuntimeIdentifier(); + + Terminal.WriteLine("Installing dev CLI as AOT binary..."); + Terminal.WriteLine($"Source: {devCliSource}"); + Terminal.WriteLine($"Output: {outputPath}/dev"); + Terminal.WriteLine($"Runtime: {rid}"); + + Directory.CreateDirectory(outputPath); + + int exitCode = await DotNet.Publish() + .WithProject(devCliSource) + .WithConfiguration("Release") + .WithRuntime(rid) + .WithSelfContained() + .WithOutput(outputPath) + .RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + Terminal.WriteErrorLine("AOT compilation failed!"); + return Value; + } + + string binaryName = rid.StartsWith("win", StringComparison.OrdinalIgnoreCase) ? "dev.exe" : "dev"; + string binaryPath = Path.Combine(outputPath, binaryName); + + if (File.Exists(binaryPath)) + { + FileInfo info = new(binaryPath); + Terminal.WriteLine($"\n✅ AOT binary installed: {binaryPath}"); + Terminal.WriteLine($" Size: {info.Length / 1024.0 / 1024.0:F1} MB"); + Terminal.WriteLine("\nRun 'direnv allow' to add ./bin to PATH, then use: dev "); + } + else + { + throw new InvalidOperationException($"Binary not found at expected location: {binaryPath}"); + } + + return Value; + } + + private static string GetRuntimeIdentifier() + { + if (OperatingSystem.IsWindows()) + { + return Environment.Is64BitOperatingSystem ? "win-x64" : "win-x86"; + } + + if (OperatingSystem.IsMacOS()) + { + return RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "osx-arm64" : "osx-x64"; + } + + if (OperatingSystem.IsLinux()) + { + return RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "linux-arm64" : "linux-x64"; + } + + return "linux-x64"; + } + } +} diff --git a/tools/dev-cli/endpoints/test.cs b/tools/dev-cli/endpoints/test.cs new file mode 100644 index 0000000..3fbc927 --- /dev/null +++ b/tools/dev-cli/endpoints/test.cs @@ -0,0 +1,77 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Runs the test suite for TimeWarp.Builder. + +namespace DevCli.Commands; + +/// +/// Run the TimeWarp.Builder test suite. +/// +[NuruRoute("test", Description = "Run the test suite")] +internal sealed class TestCommand : ICommand +{ + [Option("filter", "f", Description = "Test filter expression")] + public string? Filter { get; set; } + + [Option("no-build", null, Description = "Skip build before testing")] + public bool NoBuild { get; set; } + + [Option("verbose", "v", Description = "Verbose output")] + public bool Verbose { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(TestCommand command, CancellationToken ct) + { + string? repoRoot = Git.FindRoot() ?? throw new InvalidOperationException("Could not find git repository root (.git not found)"); + if (!File.Exists(Path.Combine(repoRoot, "timewarp-builder.slnx"))) + { + throw new InvalidOperationException("Could not find repository root (timewarp-builder.slnx not found)"); + } + + string testsDirectory = Path.Combine(repoRoot, "tests"); + + if (!Directory.Exists(testsDirectory)) + { + Terminal.WriteLine("No tests directory found. Skipping test step."); + return Value; + } + + Terminal.WriteLine("Running TimeWarp.Builder tests..."); + Terminal.WriteLine($"Working from: {repoRoot}"); + + ShellBuilder testBuilder = Shell.Builder("dotnet") + .WithArguments("test", Path.Combine(repoRoot, "timewarp-builder.slnx"), "--verbosity", command.Verbose ? "normal" : "minimal"); + + if (command.NoBuild) + { + testBuilder = testBuilder.WithArguments("--no-build"); + } + + if (command.Filter is not null) + { + testBuilder = testBuilder.WithArguments("--filter", command.Filter); + } + + int exitCode = await testBuilder.WithNoValidation().RunAsync(); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + Terminal.WriteErrorLine($"Tests failed with exit code {exitCode}"); + return Value; + } + + Terminal.WriteLine("\n✅ Tests completed successfully!"); + return Value; + } + } +} diff --git a/tools/dev-cli/endpoints/workflow.cs b/tools/dev-cli/endpoints/workflow.cs new file mode 100644 index 0000000..d253405 --- /dev/null +++ b/tools/dev-cli/endpoints/workflow.cs @@ -0,0 +1,252 @@ +// ═══════════════════════════════════════════════════════════════════════════════ +// CI COMMAND +// ═══════════════════════════════════════════════════════════════════════════════ +// Orchestrates the full CI/CD pipeline with mode detection. +// Auto-detects mode from GITHUB_EVENT_NAME or accepts explicit --mode flag. +// +// Modes: +// pr/merge: clean -> build -> test +// release: clean -> build -> check-version -> pack -> push + +namespace DevCli.Commands; + +using System.Xml.Linq; + +/// +/// Run the full CI/CD pipeline. +/// +[NuruRoute("workflow", Description = "Run full CI/CD pipeline")] +internal sealed class CiCommand : ICommand +{ + [Option("mode", "m", Description = "CI mode: pr, merge, or release (auto-detected from GITHUB_EVENT_NAME if not specified)")] + public string? Mode { get; set; } + + [Option("api-key", Description = "NuGet API key for publishing (from OIDC Trusted Publishing)")] + public string? ApiKey { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(CiCommand command, CancellationToken ct) + { + CiMode mode = DetermineMode(command.Mode); + + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine($" CI/CD Pipeline - Mode: {mode}"); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(""); + + if (mode == CiMode.Release) + { + await RunReleaseWorkflowAsync(command.ApiKey); + } + else + { + await RunPrWorkflowAsync(); + } + + return Value; + } + + private CiMode DetermineMode(string? explicitMode) + { + if (!string.IsNullOrEmpty(explicitMode)) + { + return explicitMode.ToLowerInvariant() switch + { + "pr" => CiMode.Pr, + "merge" => CiMode.Merge, + "release" => CiMode.Release, + _ => CiMode.Pr + }; + } + + string? eventName = Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME"); + + CiMode mode = eventName switch + { + "pull_request" => CiMode.Pr, + "push" => CiMode.Merge, + "release" => CiMode.Release, + "workflow_dispatch" => CiMode.Release, + _ => CiMode.Pr + }; + + string displayEventName = eventName ?? "(not set)"; + Terminal.WriteLine($"Detected GITHUB_EVENT_NAME: {displayEventName} -> Mode: {mode}"); + return mode; + } + + private async Task RunPrWorkflowAsync() + { + Terminal.WriteLine("Pipeline: clean -> build -> test"); + Terminal.WriteLine(""); + + // Step 1: Clean + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 1/3: Clean"); + Terminal.WriteLine("==============================================================================="); + CleanCommand.Handler cleanHandler = new(Terminal); + await cleanHandler.Handle(new CleanCommand(), CancellationToken.None); + + // Step 2: Build + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 2/3: Build"); + Terminal.WriteLine("==============================================================================="); + BuildCommand.Handler buildHandler = new(Terminal); + await buildHandler.Handle(new BuildCommand(), CancellationToken.None); + + // Step 3: Test + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 3/3: Test"); + Terminal.WriteLine("==============================================================================="); + TestCommand.Handler testHandler = new(Terminal); + await testHandler.Handle(new TestCommand(), CancellationToken.None); + + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Pipeline SUCCEEDED"); + Terminal.WriteLine("==============================================================================="); + } + + private async Task RunReleaseWorkflowAsync(string? apiKey) + { + Terminal.WriteLine("Pipeline: clean -> build -> check-version -> pack -> push"); + Terminal.WriteLine(""); + + string? repoRoot = Git.FindRoot() ?? + throw new InvalidOperationException("Could not find git repository root (.git not found)"); + + // Step 1: Clean + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 1/5: Clean"); + Terminal.WriteLine("==============================================================================="); + CleanCommand.Handler cleanHandler = new(Terminal); + await cleanHandler.Handle(new CleanCommand(), CancellationToken.None); + + // Step 2: Build + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 2/5: Build"); + Terminal.WriteLine("==============================================================================="); + BuildCommand.Handler buildHandler = new(Terminal); + await buildHandler.Handle(new BuildCommand(), CancellationToken.None); + + // Step 3: Check Version + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 3/5: Check Version"); + Terminal.WriteLine("==============================================================================="); + await CheckVersionAsync(repoRoot); + + // Step 4: Pack + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 4/5: Pack"); + Terminal.WriteLine("==============================================================================="); + PackCommand.Handler packHandler = new(Terminal); + await packHandler.Handle(new PackCommand(), CancellationToken.None); + + // Step 5: Push + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Step 5/5: Push to NuGet"); + Terminal.WriteLine("==============================================================================="); + await PushPackagesAsync(repoRoot, apiKey); + + Terminal.WriteLine(""); + Terminal.WriteLine("==============================================================================="); + Terminal.WriteLine(" Pipeline SUCCEEDED - Package published to NuGet.org"); + Terminal.WriteLine("==============================================================================="); + } + + private async Task CheckVersionAsync(string repoRoot) + { + string propsPath = Path.Combine(repoRoot, "source", "Directory.Build.props"); + + if (!File.Exists(propsPath)) + { + throw new FileNotFoundException($"Could not find {propsPath}"); + } + + XDocument doc = XDocument.Load(propsPath); + string? version = doc.Descendants("Version").FirstOrDefault()?.Value; + + if (string.IsNullOrEmpty(version)) + { + throw new InvalidOperationException("Could not find version in source/Directory.Build.props"); + } + + Terminal.WriteLine($"Checking if TimeWarp.Builder {version} is already published on NuGet.org..."); + + CommandOutput result = await Shell.Builder("dotnet") + .WithArguments("package", "search", "TimeWarp.Builder", "--exact-match", "--prerelease", "--source", "https://api.nuget.org/v3/index.json") + .WithNoValidation() + .CaptureAsync(); + + if (result.Stdout.Contains($"| {version} |", StringComparison.Ordinal)) + { + Terminal.WriteErrorLine($"TimeWarp.Builder {version} is already published. Increment the version in source/Directory.Build.props."); + Environment.ExitCode = 1; + throw new InvalidOperationException($"Version {version} already published."); + } + + Terminal.WriteLine($"✅ TimeWarp.Builder {version} is not yet published. Ready to release."); + } + + private async Task PushPackagesAsync(string repoRoot, string? apiKey) + { + string propsPath = Path.Combine(repoRoot, "source", "Directory.Build.props"); + XDocument doc = XDocument.Load(propsPath); + string? version = doc.Descendants("Version").FirstOrDefault()?.Value; + + if (string.IsNullOrEmpty(version)) + { + throw new InvalidOperationException("Could not determine version for push"); + } + + string artifactsDir = Path.Combine(repoRoot, "artifacts", "packages"); + string nupkgPath = Path.Combine(artifactsDir, $"TimeWarp.Builder.{version}.nupkg"); + + if (!File.Exists(nupkgPath)) + { + throw new FileNotFoundException($"Package not found: {nupkgPath}"); + } + + Terminal.WriteLine($"Pushing TimeWarp.Builder.{version}.nupkg..."); + + List args = ["nuget", "push", nupkgPath, "--source", "https://api.nuget.org/v3/index.json", "--skip-duplicate"]; + + if (!string.IsNullOrEmpty(apiKey)) + { + args.AddRange(["--api-key", apiKey]); + } + + int exitCode = await Shell.Builder("dotnet") + .WithArguments([.. args]) + .RunAsync(); + + if (exitCode != 0) + { + throw new InvalidOperationException("Failed to push TimeWarp.Builder!"); + } + + Terminal.WriteLine("\n✅ TimeWarp.Builder pushed successfully!"); + } + } +} + +internal enum CiMode +{ + Pr, + Merge, + Release +}