From 778ce64e4b9284a57703eb523972ddefd2ae7a90 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 31 Jan 2026 11:39:19 +0000 Subject: [PATCH] refactor(plugin): rename plugin files and update command structure --- .claude/skills/core/SKILL.md | 664 ------------------ .claude/skills/core/install.sh | 40 -- {plugin => .core/plugin}/commands/remember.md | 0 {plugin => .core/plugin}/hooks/prefer-core.sh | 0 {plugin => .core/plugin}/plugin.json | 7 - .../plugin}/scripts/block-docs.sh | 0 .../plugin}/scripts/capture-context.sh | 0 .../plugin}/scripts/check-debug.sh | 0 .../plugin}/scripts/extract-actionables.sh | 0 {plugin => .core/plugin}/scripts/go-format.sh | 0 .../plugin}/scripts/php-format.sh | 0 .../plugin}/scripts/post-commit-check.sh | 0 .../plugin}/scripts/pr-created.sh | 0 .../plugin}/scripts/pre-compact.sh | 0 .../plugin}/scripts/session-start.sh | 0 .../plugin}/scripts/suggest-compact.sh | 0 {plugin => .core/plugin}/skills/core.md | 0 {plugin => .core/plugin}/skills/go.md | 0 {plugin => .core/plugin}/skills/php.md | 0 pkg/ai/cmd_ai.go | 3 +- pkg/ai/cmd_commands.go | 15 +- pkg/ai/cmd_git.go | 71 +- pkg/ai/cmd_tasks.go | 89 ++- pkg/ai/cmd_updates.go | 35 +- pkg/ci/cmd_changelog.go | 11 +- pkg/ci/cmd_ci.go | 17 +- pkg/ci/cmd_commands.go | 3 +- pkg/ci/cmd_init.go | 24 +- pkg/ci/cmd_publish.go | 26 +- pkg/ci/cmd_version.go | 8 +- pkg/cli/styles.go | 58 +- pkg/dev/cmd_api.go | 6 +- pkg/dev/cmd_ci.go | 46 +- pkg/dev/cmd_commit.go | 80 +-- pkg/dev/cmd_dev.go | 5 +- pkg/dev/cmd_health.go | 32 +- pkg/dev/cmd_impact.go | 38 +- pkg/dev/cmd_issues.go | 42 +- pkg/dev/cmd_pull.go | 49 +- pkg/dev/cmd_push.go | 112 ++- pkg/dev/cmd_reviews.go | 50 +- pkg/dev/cmd_sync.go | 19 +- pkg/dev/cmd_vm.go | 187 +++-- pkg/dev/cmd_work.go | 100 ++- pkg/dev/service.go | 74 +- pkg/docs/cmd_commands.go | 7 +- pkg/docs/cmd_docs.go | 3 +- pkg/docs/cmd_list.go | 16 +- pkg/docs/cmd_scan.go | 8 +- pkg/docs/cmd_sync.go | 33 +- pkg/go/cmd_format.go | 14 +- pkg/go/cmd_go.go | 5 +- pkg/go/cmd_gotest.go | 61 +- pkg/go/cmd_qa.go | 70 +- pkg/go/cmd_tools.go | 57 +- pkg/php/cmd_build.go | 60 +- pkg/php/cmd_deploy.go | 72 +- pkg/php/cmd_dev.go | 96 +-- pkg/php/cmd_packages.go | 42 +- pkg/php/cmd_qa_runner.go | 6 +- pkg/php/cmd_quality.go | 156 ++-- pkg/php/container.go | 57 +- pkg/php/coolify.go | 59 +- pkg/php/deploy.go | 43 +- pkg/php/dockerfile.go | 49 +- pkg/php/packages.go | 41 +- pkg/php/php.go | 25 +- pkg/php/quality.go | 37 +- pkg/php/services.go | 39 +- pkg/php/ssl.go | 29 +- pkg/php/testing.go | 5 +- 71 files changed, 1130 insertions(+), 1871 deletions(-) delete mode 100644 .claude/skills/core/SKILL.md delete mode 100755 .claude/skills/core/install.sh rename {plugin => .core/plugin}/commands/remember.md (100%) rename {plugin => .core/plugin}/hooks/prefer-core.sh (100%) rename {plugin => .core/plugin}/plugin.json (94%) rename {plugin => .core/plugin}/scripts/block-docs.sh (100%) rename {plugin => .core/plugin}/scripts/capture-context.sh (100%) rename {plugin => .core/plugin}/scripts/check-debug.sh (100%) rename {plugin => .core/plugin}/scripts/extract-actionables.sh (100%) rename {plugin => .core/plugin}/scripts/go-format.sh (100%) rename {plugin => .core/plugin}/scripts/php-format.sh (100%) rename {plugin => .core/plugin}/scripts/post-commit-check.sh (100%) rename {plugin => .core/plugin}/scripts/pr-created.sh (100%) rename {plugin => .core/plugin}/scripts/pre-compact.sh (100%) rename {plugin => .core/plugin}/scripts/session-start.sh (100%) rename {plugin => .core/plugin}/scripts/suggest-compact.sh (100%) rename {plugin => .core/plugin}/skills/core.md (100%) rename {plugin => .core/plugin}/skills/go.md (100%) rename {plugin => .core/plugin}/skills/php.md (100%) diff --git a/.claude/skills/core/SKILL.md b/.claude/skills/core/SKILL.md deleted file mode 100644 index 4d5454ca..00000000 --- a/.claude/skills/core/SKILL.md +++ /dev/null @@ -1,664 +0,0 @@ ---- -name: core -description: Use when working in host-uk repositories, running tests, building, releasing, or managing multi-repo workflows. Provides the core CLI command reference. ---- - -# Core CLI - -The `core` command provides a unified interface for Go/Wails development, multi-repo management, and deployment. - -**Rule:** Always prefer `core ` over raw commands. It handles environment setup, output formatting, and cross-platform concerns. - -## Command Quick Reference - -| Task | Command | Notes | -|------|---------|-------| -| Run Go tests | `core go test` | Sets macOS deployment target, filters warnings | -| Run Go tests with coverage | `core go cov` | HTML report, thresholds | -| Format Go code | `core go fmt --fix` | Uses goimports/gofmt | -| Lint Go code | `core go lint` | Uses golangci-lint | -| Tidy Go modules | `core go mod tidy` | go mod tidy wrapper | -| Sync Go workspace | `core go work sync` | go work sync wrapper | -| Install Go binary | `core go install` | Auto-detects cmd/ | -| Run PHP tests | `core php test` | Auto-detects Pest/PHPUnit | -| Start PHP dev server | `core php dev` | FrankenPHP + Vite + Horizon + Reverb | -| Format PHP code | `core php fmt --fix` | Laravel Pint | -| Deploy PHP app | `core php deploy` | Coolify deployment | -| Build project | `core build` | Auto-detects project type | -| Build for targets | `core build --targets linux/amd64,darwin/arm64` | Cross-compile | -| Build SDK | `core build sdk` | Generate API clients from OpenAPI | -| Preview release | `core ci` | Dry-run publish (safe default) | -| Publish release | `core ci --we-are-go-for-launch` | Actually publish artifacts | -| Check environment | `core doctor` | Verify tools installed | -| Multi-repo status | `core dev health` | Quick summary across repos | -| Multi-repo workflow | `core dev work` | Status + commit + push | -| Commit dirty repos | `core dev commit` | Claude-assisted commit messages | -| Push repos | `core dev push` | Push repos with unpushed commits | -| Pull repos | `core dev pull` | Pull repos that are behind | -| List issues | `core dev issues` | Open issues across repos | -| List PRs | `core dev reviews` | PRs needing review | -| Check CI | `core dev ci` | GitHub Actions status | -| Validate OpenAPI | `core sdk validate` | Validate OpenAPI spec | -| Check API changes | `core sdk diff` | Detect breaking API changes | -| Sync docs | `core docs sync` | Sync docs across repos | -| Search packages | `core pkg search ` | GitHub search for core-* repos | -| Install package | `core pkg install ` | Clone and register package | -| Update packages | `core pkg update` | Pull latest for all packages | -| Run VM | `core vm run ` | Run LinuxKit VM | - -## Building - -**Always use `core build` instead of `go build`.** - -```bash -# Auto-detect and build -core build - -# Build for specific targets -core build --targets linux/amd64,darwin/arm64 - -# Build Docker image -core build --type docker - -# Build LinuxKit image -core build --type linuxkit --format qcow2-bios - -# CI mode (JSON output) -core build --ci -``` - -**Why:** Handles cross-compilation, code signing, archiving, checksums, and CI output formatting. - -## Releasing - -Build and publish are **separated** to prevent accidental releases: - -```bash -# Step 1: Build artifacts (safe - no publishing) -core build -core build sdk - -# Step 2: Preview publish (default is dry-run) -core ci # Dry-run: shows what would be published - -# Step 3: Actually publish (explicit flag required) -core ci --we-are-go-for-launch # Actually publish to targets -core ci --we-are-go-for-launch --draft # Publish as draft -core ci --we-are-go-for-launch --prerelease # Publish as prerelease -``` - -**Why safe by default?** `core ci` always does a dry-run unless you explicitly say `--we-are-go-for-launch`. - -```bash -# Release workflow utilities -core ci init # Initialize .core/release.yaml -core ci changelog # Generate changelog from commits -core ci version # Show determined version -``` - -## Multi-Repo Workflow - -When working across host-uk repositories: - -```bash -# Quick health check -core dev health -# Output: "18 repos │ clean │ synced" - -# Full status table -core dev work --status - -# Commit + push workflow -core dev work - -# Commit dirty repos with Claude -core dev commit - -# Push repos with unpushed commits -core dev push - -# Pull repos that are behind -core dev pull -``` - -### Dependency Analysis - -```bash -# What depends on core-php? -core dev impact core-php -``` - -## GitHub Integration - -Requires `gh` CLI authenticated. - -```bash -# Open issues across all repos -core dev issues - -# Include closed issues -core dev issues --all - -# PRs needing review -core dev reviews - -# CI status -core dev ci -``` - -## SDK Generation - -Generate API clients from OpenAPI specs: - -```bash -# Generate all configured SDKs -core build sdk - -# Generate specific language -core build sdk --lang typescript -core build sdk --lang php - -# Specify OpenAPI spec -core build sdk --spec ./openapi.yaml - -# Preview without generating -core build sdk --dry-run -``` - -## SDK Validation - -Validate specs and check for breaking changes: - -```bash -# Validate OpenAPI spec -core sdk validate -core sdk validate --spec ./api.yaml - -# Check for breaking API changes -core sdk diff --base v1.0.0 -core sdk diff --base ./old-api.yaml --spec ./new-api.yaml -``` - -## Documentation - -```bash -# List docs across repos -core docs list - -# Sync docs to central location -core docs sync -``` - -## Environment Setup - -```bash -# Check development environment -core doctor - -# Clone all repos from registry -core setup -``` - -## Package Management - -Manage host-uk/core-* packages and repositories. - -```bash -# Search GitHub for packages -core pkg search -core pkg search core- # Find all core-* packages -core pkg search --org host-uk # Search specific org - -# Install/clone a package -core pkg install core-api -core pkg install host-uk/core-api # Full name - -# List installed packages -core pkg list -core pkg list --format json # JSON output - -# Update installed packages -core pkg update # Update all -core pkg update core-api # Update specific package - -# Check for outdated packages -core pkg outdated -``` - -## Go Development - -**Always use `core go` commands instead of raw go commands.** - -### Quick Reference - -| Task | Command | Notes | -|------|---------|-------| -| Run tests | `core go test` | Filters warnings, colour output | -| Coverage report | `core go cov` | HTML report, thresholds | -| Format code | `core go fmt --fix` | Uses goimports if available | -| Lint code | `core go lint` | Uses golangci-lint | -| Install binary | `core go install` | Auto-detects cmd/, --no-cgo option | -| Tidy modules | `core go mod tidy` | go mod tidy | -| Sync workspace | `core go work sync` | go work sync | - -### Installing - -```bash -# Install current module (auto-detects cmd/ subdirs) -core go install - -# Install specific path -core go install ./cmd/core - -# Pure Go, no C dependencies -core go install --no-cgo - -# Verbose output -core go install -v -``` - -### Coverage - -```bash -# Run tests with coverage summary -core go cov - -# Generate HTML report -core go cov --html - -# Generate and open in browser -core go cov --open - -# Fail if coverage below threshold -core go cov --threshold 80 - -# Specific package -core go cov --pkg ./pkg/release -``` - -### Testing - -```bash -# Run all tests -core go test - -# With coverage -core go test --coverage - -# Specific package -core go test --pkg ./pkg/errors - -# Run specific tests -core go test --run TestHash - -# Short tests only -core go test --short - -# Race detection -core go test --race - -# JSON output for CI -core go test --json - -# Verbose -core go test -v -``` - -**Why:** Sets `CGO_ENABLED=0` and `MACOSX_DEPLOYMENT_TARGET=26.0`, filters linker warnings, provides colour-coded coverage. - -### Formatting & Linting - -```bash -# Check formatting -core go fmt - -# Fix formatting -core go fmt --fix - -# Show diff -core go fmt --diff - -# Run linter -core go lint - -# Lint with auto-fix -core go lint --fix -``` - -### Module Management - -```bash -# Tidy go.mod -core go mod tidy - -# Download dependencies -core go mod download - -# Verify dependencies -core go mod verify - -# Show dependency graph -core go mod graph -``` - -### Workspace Management - -```bash -# Sync workspace -core go work sync - -# Initialize workspace -core go work init - -# Add module to workspace -core go work use ./pkg/mymodule - -# Auto-add all modules -core go work use -``` - -## PHP Development - -**Always use `core php` commands instead of raw artisan/composer/phpunit.** - -### Quick Reference - -| Task | Command | Notes | -|------|---------|-------| -| Start dev environment | `core php dev` | FrankenPHP + Vite + Horizon + Reverb + Redis | -| Run PHP tests | `core php test` | Auto-detects Pest/PHPUnit | -| Format code | `core php fmt --fix` | Laravel Pint | -| Static analysis | `core php analyse` | PHPStan/Larastan | -| Build Docker image | `core php build` | Production-ready FrankenPHP | -| Deploy to Coolify | `core php deploy` | With status tracking | - -### Development Server - -```bash -# Start full Laravel dev environment -core php dev - -# Start with HTTPS (uses mkcert) -core php dev --https - -# Skip specific services -core php dev --no-vite --no-horizon - -# Custom port -core php dev --port 9000 -``` - -**Services orchestrated:** -- FrankenPHP/Octane (port 8000, HTTPS on 443) -- Vite dev server (port 5173) -- Laravel Horizon (queue workers) -- Laravel Reverb (WebSocket, port 8080) -- Redis (port 6379) - -```bash -# View logs -core php logs -core php logs --service frankenphp - -# Check status -core php status - -# Stop all services -core php stop - -# Setup SSL certificates -core php ssl -core php ssl --domain myapp.test -``` - -### Testing - -```bash -# Run all tests (auto-detects Pest/PHPUnit) -core php test - -# Run in parallel -core php test --parallel - -# With coverage -core php test --coverage - -# Filter tests -core php test --filter UserTest -core php test --group api -``` - -### Code Quality - -```bash -# Check formatting (dry-run) -core php fmt - -# Auto-fix formatting -core php fmt --fix - -# Show diff -core php fmt --diff - -# Run static analysis -core php analyse - -# Max strictness -core php analyse --level 9 -``` - -### Building & Deployment - -```bash -# Build Docker image -core php build -core php build --name myapp --tag v1.0 - -# Build for specific platform -core php build --platform linux/amd64 - -# Build LinuxKit image -core php build --type linuxkit --format iso - -# Run production container -core php serve --name myapp -core php serve --name myapp -d # Detached - -# Open shell in container -core php shell myapp -``` - -### Coolify Deployment - -```bash -# Deploy to production -core php deploy - -# Deploy to staging -core php deploy --staging - -# Wait for completion -core php deploy --wait - -# Check deployment status -core php deploy:status - -# List recent deployments -core php deploy:list - -# Rollback -core php deploy:rollback -core php deploy:rollback --id abc123 -``` - -**Required .env configuration:** -```env -COOLIFY_URL=https://coolify.example.com -COOLIFY_TOKEN=your-api-token -COOLIFY_APP_ID=production-app-id -COOLIFY_STAGING_APP_ID=staging-app-id -``` - -### Package Management - -```bash -# Link local packages for development -core php packages link ../my-package -core php packages link ../pkg-a ../pkg-b - -# List linked packages -core php packages list - -# Update linked packages -core php packages update - -# Unlink packages -core php packages unlink vendor/my-package -``` - -## VM Management - -LinuxKit VMs are lightweight, immutable VMs built from YAML templates. - -```bash -# Run LinuxKit image -core vm run server.iso - -# Run with options -core vm run -d --memory 2048 --cpus 4 image.iso - -# Run from template -core vm run --template core-dev --var SSH_KEY="ssh-rsa AAAA..." - -# List running VMs -core vm ps -core vm ps -a # Include stopped - -# Stop VM -core vm stop - -# View logs -core vm logs -core vm logs -f # Follow - -# Execute command in VM -core vm exec ls -la -core vm exec /bin/sh - -# Manage templates -core vm templates # List templates -core vm templates show # Show template content -core vm templates vars # Show template variables -``` - -## Decision Tree - -``` -Go project? - └── Run tests: core go test [--coverage] - └── Format: core go fmt --fix - └── Lint: core go lint - └── Tidy modules: core go mod tidy - └── Build: core build [--targets ] - └── Build SDK: core build sdk - └── Preview publish: core ci - └── Publish: core ci --we-are-go-for-launch - -PHP/Laravel project? - └── Start dev: core php dev [--https] - └── Run tests: core php test [--parallel] - └── Format: core php fmt --fix - └── Analyse: core php analyse - └── Build image: core php build - └── Deploy: core php deploy [--staging] - -Working across multiple repos? - └── Quick check: core dev health - └── Full workflow: core dev work - └── Just commit: core dev commit - └── Just push: core dev push - -Need GitHub info? - └── Issues: core dev issues - └── PRs: core dev reviews - └── CI: core dev ci - -Setting up environment? - └── Check: core doctor - └── Clone all: core setup - -Managing packages? - └── Search: core pkg search - └── Install: core pkg install - └── Update: core pkg update - └── Check outdated: core pkg outdated -``` - -## Common Mistakes - -| Wrong | Right | Why | -|-------|-------|-----| -| `go test ./...` | `core go test` | CGO disabled, filters warnings, coverage | -| `go fmt ./...` | `core go fmt --fix` | Uses goimports, consistent | -| `golangci-lint run` | `core go lint` | Consistent interface | -| `go build` | `core build` | Missing cross-compile, signing, checksums | -| `php artisan serve` | `core php dev` | Missing Vite, Horizon, Reverb, Redis | -| `./vendor/bin/pest` | `core php test` | Inconsistent invocation | -| `./vendor/bin/pint` | `core php fmt --fix` | Consistent interface | -| `git status` in each repo | `core dev health` | Slow, manual | -| `gh pr list` per repo | `core dev reviews` | Aggregated view | -| Manual commits across repos | `core dev commit` | Consistent messages, Co-Authored-By | -| Manual Coolify deploys | `core php deploy` | Tracked, scriptable | -| Raw `linuxkit run` | `core vm run` | Unified interface, templates | -| `gh repo clone` | `core pkg install` | Auto-detects org, adds to registry | -| Manual GitHub search | `core pkg search` | Filtered to org, formatted output | -| `core ci` without build | `core build && core ci` | Build first, then publish | -| `core sdk generate` | `core build sdk` | SDK generation moved to build | - -## Configuration - -Core reads from `.core/` directory: - -``` -.core/ -├── release.yaml # Release targets -├── build.yaml # Build settings -└── linuxkit/ # LinuxKit templates -``` - -And `repos.yaml` in workspace root for multi-repo management. - -## Build Variants - -Core supports build tags for different deployment contexts: - -```bash -# Full development binary (default) -go build -o core ./cmd/core/ - -# CI-only binary (minimal attack surface) -go build -tags ci -o core-ci ./cmd/core/ -``` - -| Variant | Commands | Use Case | -|---------|----------|----------| -| `core` (default) | All commands | Development, local workflow | -| `core-ci` | build, ci, sdk, doctor | CI pipelines, production builds | - -The CI variant excludes development tools (go, php, dev, pkg, vm, etc.) for a smaller attack surface in automated environments. - -## Installation - -```bash -# Go install (full binary) -CGO_ENABLED=0 go install github.com/host-uk/core/cmd/core@latest - -# Or from source -cd /path/to/core -CGO_ENABLED=0 go install ./cmd/core/ - -# CI variant -CGO_ENABLED=0 go build -tags ci -o /usr/local/bin/core-ci ./cmd/core/ -``` - -Verify: `core doctor` \ No newline at end of file diff --git a/.claude/skills/core/install.sh b/.claude/skills/core/install.sh deleted file mode 100755 index eba38591..00000000 --- a/.claude/skills/core/install.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# Install the core skill globally for Claude Code -# -# Usage: -# curl -fsSL https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/install.sh | bash -# -# Or if you have the repo cloned: -# ./.claude/skills/core/install.sh - -set -e - -SKILL_DIR="$HOME/.claude/skills/core" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Check if running from repo or downloading -if [ -f "$SCRIPT_DIR/SKILL.md" ]; then - SOURCE_DIR="$SCRIPT_DIR" -else - # Download from GitHub - TEMP_DIR=$(mktemp -d) - trap "rm -rf $TEMP_DIR" EXIT - - echo "Downloading core skill..." - curl -fsSL "https://raw.githubusercontent.com/host-uk/core/main/.claude/skills/core/SKILL.md" -o "$TEMP_DIR/SKILL.md" - SOURCE_DIR="$TEMP_DIR" -fi - -# Create skills directory if needed -mkdir -p "$SKILL_DIR" - -# Copy skill file -cp "$SOURCE_DIR/SKILL.md" "$SKILL_DIR/SKILL.md" - -echo "Installed core skill to $SKILL_DIR" -echo "" -echo "Usage:" -echo " - Claude will auto-invoke when working in host-uk repos" -echo " - Or type /core to invoke manually" -echo "" -echo "Commands available: core test, core build, core ci, core work, etc." \ No newline at end of file diff --git a/plugin/commands/remember.md b/.core/plugin/commands/remember.md similarity index 100% rename from plugin/commands/remember.md rename to .core/plugin/commands/remember.md diff --git a/plugin/hooks/prefer-core.sh b/.core/plugin/hooks/prefer-core.sh similarity index 100% rename from plugin/hooks/prefer-core.sh rename to .core/plugin/hooks/prefer-core.sh diff --git a/plugin/plugin.json b/.core/plugin/plugin.json similarity index 94% rename from plugin/plugin.json rename to .core/plugin/plugin.json index 027d50dd..2f79b85b 100644 --- a/plugin/plugin.json +++ b/.core/plugin/plugin.json @@ -98,12 +98,5 @@ "description": "Warn about uncommitted work after git commit" } ] - }, - "mcp": { - "core": { - "command": "core", - "args": ["mcp", "serve"], - "description": "Core CLI MCP server for multi-repo operations" - } } } diff --git a/plugin/scripts/block-docs.sh b/.core/plugin/scripts/block-docs.sh similarity index 100% rename from plugin/scripts/block-docs.sh rename to .core/plugin/scripts/block-docs.sh diff --git a/plugin/scripts/capture-context.sh b/.core/plugin/scripts/capture-context.sh similarity index 100% rename from plugin/scripts/capture-context.sh rename to .core/plugin/scripts/capture-context.sh diff --git a/plugin/scripts/check-debug.sh b/.core/plugin/scripts/check-debug.sh similarity index 100% rename from plugin/scripts/check-debug.sh rename to .core/plugin/scripts/check-debug.sh diff --git a/plugin/scripts/extract-actionables.sh b/.core/plugin/scripts/extract-actionables.sh similarity index 100% rename from plugin/scripts/extract-actionables.sh rename to .core/plugin/scripts/extract-actionables.sh diff --git a/plugin/scripts/go-format.sh b/.core/plugin/scripts/go-format.sh similarity index 100% rename from plugin/scripts/go-format.sh rename to .core/plugin/scripts/go-format.sh diff --git a/plugin/scripts/php-format.sh b/.core/plugin/scripts/php-format.sh similarity index 100% rename from plugin/scripts/php-format.sh rename to .core/plugin/scripts/php-format.sh diff --git a/plugin/scripts/post-commit-check.sh b/.core/plugin/scripts/post-commit-check.sh similarity index 100% rename from plugin/scripts/post-commit-check.sh rename to .core/plugin/scripts/post-commit-check.sh diff --git a/plugin/scripts/pr-created.sh b/.core/plugin/scripts/pr-created.sh similarity index 100% rename from plugin/scripts/pr-created.sh rename to .core/plugin/scripts/pr-created.sh diff --git a/plugin/scripts/pre-compact.sh b/.core/plugin/scripts/pre-compact.sh similarity index 100% rename from plugin/scripts/pre-compact.sh rename to .core/plugin/scripts/pre-compact.sh diff --git a/plugin/scripts/session-start.sh b/.core/plugin/scripts/session-start.sh similarity index 100% rename from plugin/scripts/session-start.sh rename to .core/plugin/scripts/session-start.sh diff --git a/plugin/scripts/suggest-compact.sh b/.core/plugin/scripts/suggest-compact.sh similarity index 100% rename from plugin/scripts/suggest-compact.sh rename to .core/plugin/scripts/suggest-compact.sh diff --git a/plugin/skills/core.md b/.core/plugin/skills/core.md similarity index 100% rename from plugin/skills/core.md rename to .core/plugin/skills/core.md diff --git a/plugin/skills/go.md b/.core/plugin/skills/go.md similarity index 100% rename from plugin/skills/go.md rename to .core/plugin/skills/go.md diff --git a/plugin/skills/php.md b/.core/plugin/skills/php.md similarity index 100% rename from plugin/skills/php.md rename to .core/plugin/skills/php.md diff --git a/pkg/ai/cmd_ai.go b/pkg/ai/cmd_ai.go index 631db0e6..f54e02d7 100644 --- a/pkg/ai/cmd_ai.go +++ b/pkg/ai/cmd_ai.go @@ -4,7 +4,6 @@ package ai import ( "github.com/host-uk/core/pkg/cli" - "github.com/spf13/cobra" ) // Style aliases from shared package @@ -35,7 +34,7 @@ var ( ) // AddAgenticCommands adds the agentic task management commands to the ai command. -func AddAgenticCommands(parent *cobra.Command) { +func AddAgenticCommands(parent *cli.Command) { // Task listing and viewing addTasksCommand(parent) addTaskCommand(parent) diff --git a/pkg/ai/cmd_commands.go b/pkg/ai/cmd_commands.go index ccf18ce0..45e5aaf2 100644 --- a/pkg/ai/cmd_commands.go +++ b/pkg/ai/cmd_commands.go @@ -13,37 +13,36 @@ package ai import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) func init() { cli.RegisterCommands(AddAICommands) } -var aiCmd = &cobra.Command{ +var aiCmd = &cli.Command{ Use: "ai", Short: i18n.T("cmd.ai.short"), Long: i18n.T("cmd.ai.long"), } -var claudeCmd = &cobra.Command{ +var claudeCmd = &cli.Command{ Use: "claude", Short: i18n.T("cmd.ai.claude.short"), Long: i18n.T("cmd.ai.claude.long"), } -var claudeRunCmd = &cobra.Command{ +var claudeRunCmd = &cli.Command{ Use: "run", Short: i18n.T("cmd.ai.claude.run.short"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runClaudeCode() }, } -var claudeConfigCmd = &cobra.Command{ +var claudeConfigCmd = &cli.Command{ Use: "config", Short: i18n.T("cmd.ai.claude.config.short"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return showClaudeConfig() }, } @@ -61,7 +60,7 @@ func initCommands() { } // AddAICommands registers the 'ai' command and all subcommands. -func AddAICommands(root *cobra.Command) { +func AddAICommands(root *cli.Command) { initCommands() root.AddCommand(aiCmd) } diff --git a/pkg/ai/cmd_git.go b/pkg/ai/cmd_git.go index 8451b4c7..9efbcfdd 100644 --- a/pkg/ai/cmd_git.go +++ b/pkg/ai/cmd_git.go @@ -5,15 +5,14 @@ package ai import ( "bytes" "context" - "fmt" "os" "os/exec" "strings" "time" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // task:commit command flags @@ -31,21 +30,21 @@ var ( taskPRBase string ) -var taskCommitCmd = &cobra.Command{ +var taskCommitCmd = &cli.Command{ Use: "task:commit [task-id]", Short: i18n.T("cmd.ai.task_commit.short"), Long: i18n.T("cmd.ai.task_commit.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Args: cli.ExactArgs(1), + RunE: func(cmd *cli.Command, args []string) error { taskID := args[0] if taskCommitMessage == "" { - return fmt.Errorf("commit message required") + return cli.Err("commit message required") } cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -56,67 +55,67 @@ var taskCommitCmd = &cobra.Command{ // Get task details task, err := client.GetTask(ctx, taskID) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "task"), err) + return cli.WrapVerb(err, "get", "task") } // Build commit message with optional scope commitType := inferCommitType(task.Labels) var fullMessage string if taskCommitScope != "" { - fullMessage = fmt.Sprintf("%s(%s): %s", commitType, taskCommitScope, taskCommitMessage) + fullMessage = cli.Sprintf("%s(%s): %s", commitType, taskCommitScope, taskCommitMessage) } else { - fullMessage = fmt.Sprintf("%s: %s", commitType, taskCommitMessage) + fullMessage = cli.Sprintf("%s: %s", commitType, taskCommitMessage) } // Get current directory cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } // Check for uncommitted changes hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.check", "git status"), err) + return cli.WrapVerb(err, "check", "git status") } if !hasChanges { - fmt.Println("No changes to commit") + cli.Text("No changes to commit") return nil } // Create commit - fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("create", "commit for "+taskID)) + cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("create", "commit for "+taskID)) if err := agentic.AutoCommit(ctx, task, cwd, fullMessage); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.commit"), err) + return cli.WrapAction(err, "commit") } - fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.commit")+":", fullMessage) + cli.Print("%s %s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.commit")+":", fullMessage) // Push if requested if taskCommitPush { - fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.Progress("push")) + cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.Progress("push")) if err := agentic.PushChanges(ctx, cwd); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.push"), err) + return cli.WrapAction(err, "push") } - fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.push", "changes")) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.push", "changes")) } return nil }, } -var taskPRCmd = &cobra.Command{ +var taskPRCmd = &cli.Command{ Use: "task:pr [task-id]", Short: i18n.T("cmd.ai.task_pr.short"), Long: i18n.T("cmd.ai.task_pr.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Args: cli.ExactArgs(1), + RunE: func(cmd *cli.Command, args []string) error { taskID := args[0] cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -127,31 +126,31 @@ var taskPRCmd = &cobra.Command{ // Get task details task, err := client.GetTask(ctx, taskID) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "task"), err) + return cli.WrapVerb(err, "get", "task") } // Get current directory cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } // Check current branch branch, err := agentic.GetCurrentBranch(ctx, cwd) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "branch"), err) + return cli.WrapVerb(err, "get", "branch") } if branch == "main" || branch == "master" { - return fmt.Errorf("cannot create PR from %s branch", branch) + return cli.Err("cannot create PR from %s branch", branch) } // Push current branch - fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("push", branch)) + cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("push", branch)) if err := agentic.PushChanges(ctx, cwd); err != nil { // Try setting upstream if _, err := runGitCommand(cwd, "push", "-u", "origin", branch); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.push", "branch"), err) + return cli.WrapVerb(err, "push", "branch") } } @@ -167,14 +166,14 @@ var taskPRCmd = &cobra.Command{ } // Create PR - fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("create", "PR")) + cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.ProgressSubject("create", "PR")) prURL, err := agentic.CreatePR(ctx, task, cwd, opts) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.create", "PR"), err) + return cli.WrapVerb(err, "create", "PR") } - fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.create", "PR")) - fmt.Printf(" %s %s\n", i18n.Label("url"), prURL) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.create", "PR")) + cli.Print(" %s %s\n", i18n.Label("url"), prURL) return nil }, @@ -193,12 +192,12 @@ func initGitFlags() { taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", i18n.T("cmd.ai.task_pr.flag.base")) } -func addTaskCommitCommand(parent *cobra.Command) { +func addTaskCommitCommand(parent *cli.Command) { initGitFlags() parent.AddCommand(taskCommitCmd) } -func addTaskPRCommand(parent *cobra.Command) { +func addTaskPRCommand(parent *cli.Command) { parent.AddCommand(taskPRCmd) } @@ -240,7 +239,7 @@ func runGitCommand(dir string, args ...string) (string, error) { if err := cmd.Run(); err != nil { if stderr.Len() > 0 { - return "", fmt.Errorf("%w: %s", err, stderr.String()) + return "", cli.Wrap(err, stderr.String()) } return "", err } diff --git a/pkg/ai/cmd_tasks.go b/pkg/ai/cmd_tasks.go index f7e1cc27..3ee62d48 100644 --- a/pkg/ai/cmd_tasks.go +++ b/pkg/ai/cmd_tasks.go @@ -4,15 +4,14 @@ package ai import ( "context" - "fmt" "os" "sort" "strings" "time" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // tasks command flags @@ -31,11 +30,11 @@ var ( taskShowContext bool ) -var tasksCmd = &cobra.Command{ +var tasksCmd = &cli.Command{ Use: "tasks", Short: i18n.T("cmd.ai.tasks.short"), Long: i18n.T("cmd.ai.tasks.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { limit := tasksLimit if limit == 0 { limit = 20 @@ -43,7 +42,7 @@ var tasksCmd = &cobra.Command{ cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -68,11 +67,11 @@ var tasksCmd = &cobra.Command{ tasks, err := client.ListTasks(ctx, opts) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.list", "tasks"), err) + return cli.WrapVerb(err, "list", "tasks") } if len(tasks) == 0 { - fmt.Println(i18n.T("cmd.ai.tasks.none_found")) + cli.Text(i18n.T("cmd.ai.tasks.none_found")) return nil } @@ -81,14 +80,14 @@ var tasksCmd = &cobra.Command{ }, } -var taskCmd = &cobra.Command{ +var taskCmd = &cli.Command{ Use: "task [task-id]", Short: i18n.T("cmd.ai.task.short"), Long: i18n.T("cmd.ai.task.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -111,11 +110,11 @@ var taskCmd = &cobra.Command{ Limit: 50, }) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.list", "tasks"), err) + return cli.WrapVerb(err, "list", "tasks") } if len(tasks) == 0 { - fmt.Println(i18n.T("cmd.ai.task.no_pending")) + cli.Text(i18n.T("cmd.ai.task.no_pending")) return nil } @@ -135,12 +134,12 @@ var taskCmd = &cobra.Command{ taskClaim = true // Auto-select implies claiming } else { if taskID == "" { - return fmt.Errorf("%s", i18n.T("cmd.ai.task.id_required")) + return cli.Err(i18n.T("cmd.ai.task.id_required")) } task, err = client.GetTask(ctx, taskID) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "task"), err) + return cli.WrapVerb(err, "get", "task") } } @@ -149,25 +148,25 @@ var taskCmd = &cobra.Command{ cwd, _ := os.Getwd() taskCtx, err := agentic.BuildTaskContext(task, cwd) if err != nil { - fmt.Printf("%s %s: %s\n", errorStyle.Render(">>"), i18n.T("i18n.fail.build", "context"), err) + cli.Print("%s %s: %s\n", errorStyle.Render(">>"), i18n.T("i18n.fail.build", "context"), err) } else { - fmt.Println(taskCtx.FormatContext()) + cli.Text(taskCtx.FormatContext()) } } else { printTaskDetails(task) } if taskClaim && task.Status == agentic.StatusPending { - fmt.Println() - fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming")) + cli.Line("") + cli.Print("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming")) claimedTask, err := client.ClaimTask(ctx, task.ID) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.claim", "task"), err) + return cli.WrapVerb(err, "claim", "task") } - fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.claim", "task")) - fmt.Printf(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status)) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.claim", "task")) + cli.Print(" %s %s\n", i18n.Label("status"), formatTaskStatus(claimedTask.Status)) } return nil @@ -188,17 +187,17 @@ func initTasksFlags() { taskCmd.Flags().BoolVar(&taskShowContext, "context", false, i18n.T("cmd.ai.task.flag.context")) } -func addTasksCommand(parent *cobra.Command) { +func addTasksCommand(parent *cli.Command) { initTasksFlags() parent.AddCommand(tasksCmd) } -func addTaskCommand(parent *cobra.Command) { +func addTaskCommand(parent *cli.Command) { parent.AddCommand(taskCmd) } func printTaskList(tasks []agentic.Task) { - fmt.Printf("\n%s\n\n", i18n.T("cmd.ai.tasks.found", map[string]interface{}{"Count": len(tasks)})) + cli.Print("\n%s\n\n", i18n.T("cmd.ai.tasks.found", map[string]interface{}{"Count": len(tasks)})) for _, task := range tasks { id := taskIDStyle.Render(task.ID) @@ -206,56 +205,56 @@ func printTaskList(tasks []agentic.Task) { priority := formatTaskPriority(task.Priority) status := formatTaskStatus(task.Status) - line := fmt.Sprintf(" %s %s %s %s", id, priority, status, title) + line := cli.Sprintf(" %s %s %s %s", id, priority, status, title) if len(task.Labels) > 0 { labels := taskLabelStyle.Render("[" + strings.Join(task.Labels, ", ") + "]") line += " " + labels } - fmt.Println(line) + cli.Text(line) } - fmt.Println() - fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint"))) + cli.Line("") + cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint"))) } func printTaskDetails(task *agentic.Task) { - fmt.Println() - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID)) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title)) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority)) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("status")), formatTaskStatus(task.Status)) + cli.Line("") + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority)) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), formatTaskStatus(task.Status)) if task.Project != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("project")), task.Project) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("project")), task.Project) } if len(task.Labels) > 0 { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.labels")), taskLabelStyle.Render(strings.Join(task.Labels, ", "))) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.labels")), taskLabelStyle.Render(strings.Join(task.Labels, ", "))) } if task.ClaimedBy != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.claimed_by")), task.ClaimedBy) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.claimed_by")), task.ClaimedBy) } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt)) - fmt.Println() - fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description"))) - fmt.Println(task.Description) + cli.Line("") + cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description"))) + cli.Text(task.Description) if len(task.Files) > 0 { - fmt.Println() - fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files"))) + cli.Line("") + cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files"))) for _, f := range task.Files { - fmt.Printf(" - %s\n", f) + cli.Print(" - %s\n", f) } } if len(task.Dependencies) > 0 { - fmt.Println() - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", ")) + cli.Line("") + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", ")) } } diff --git a/pkg/ai/cmd_updates.go b/pkg/ai/cmd_updates.go index 30135a11..df533207 100644 --- a/pkg/ai/cmd_updates.go +++ b/pkg/ai/cmd_updates.go @@ -4,12 +4,11 @@ package ai import ( "context" - "fmt" "time" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // task:update command flags @@ -26,21 +25,21 @@ var ( taskCompleteErrorMsg string ) -var taskUpdateCmd = &cobra.Command{ +var taskUpdateCmd = &cli.Command{ Use: "task:update [task-id]", Short: i18n.T("cmd.ai.task_update.short"), Long: i18n.T("cmd.ai.task_update.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Args: cli.ExactArgs(1), + RunE: func(cmd *cli.Command, args []string) error { taskID := args[0] if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { - return fmt.Errorf("%s", i18n.T("cmd.ai.task_update.flag_required")) + return cli.Err(i18n.T("cmd.ai.task_update.flag_required")) } cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -57,25 +56,25 @@ var taskUpdateCmd = &cobra.Command{ } if err := client.UpdateTask(ctx, taskID, update); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.update", "task"), err) + return cli.WrapVerb(err, "update", "task") } - fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.update", "task")) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.update", "task")) return nil }, } -var taskCompleteCmd = &cobra.Command{ +var taskCompleteCmd = &cli.Command{ Use: "task:complete [task-id]", Short: i18n.T("cmd.ai.task_complete.short"), Long: i18n.T("cmd.ai.task_complete.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Args: cli.ExactArgs(1), + RunE: func(cmd *cli.Command, args []string) error { taskID := args[0] cfg, err := agentic.LoadConfig("") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } client := agentic.NewClientFromConfig(cfg) @@ -90,13 +89,13 @@ var taskCompleteCmd = &cobra.Command{ } if err := client.CompleteTask(ctx, taskID, result); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.complete", "task"), err) + return cli.WrapVerb(err, "complete", "task") } if taskCompleteFailed { - fmt.Printf("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID})) + cli.Print("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID})) } else { - fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.complete", "task")) + cli.Print("%s %s\n", successStyle.Render(">>"), i18n.T("i18n.done.complete", "task")) } return nil }, @@ -114,11 +113,11 @@ func initUpdatesFlags() { taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", i18n.T("cmd.ai.task_complete.flag.error")) } -func addTaskUpdateCommand(parent *cobra.Command) { +func addTaskUpdateCommand(parent *cli.Command) { initUpdatesFlags() parent.AddCommand(taskUpdateCmd) } -func addTaskCompleteCommand(parent *cobra.Command) { +func addTaskCompleteCommand(parent *cli.Command) { parent.AddCommand(taskCompleteCmd) } diff --git a/pkg/ci/cmd_changelog.go b/pkg/ci/cmd_changelog.go index dd8ab59f..fb435223 100644 --- a/pkg/ci/cmd_changelog.go +++ b/pkg/ci/cmd_changelog.go @@ -1,10 +1,9 @@ package ci import ( - "fmt" "os" - "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/release" ) @@ -12,21 +11,21 @@ import ( func runChangelog(fromRef, toRef string) error { projectDir, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } // Load config for changelog settings cfg, err := release.LoadConfig(projectDir) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } // Generate changelog changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.generate", "changelog"), err) + return cli.WrapVerb(err, "generate", "changelog") } - fmt.Println(changelog) + cli.Text(changelog) return nil } diff --git a/pkg/ci/cmd_ci.go b/pkg/ci/cmd_ci.go index 12721931..6a31fcbe 100644 --- a/pkg/ci/cmd_ci.go +++ b/pkg/ci/cmd_ci.go @@ -4,7 +4,6 @@ package ci import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // Style aliases from shared @@ -30,39 +29,39 @@ var ( changelogToRef string ) -var ciCmd = &cobra.Command{ +var ciCmd = &cli.Command{ Use: "ci", Short: i18n.T("cmd.ci.short"), Long: i18n.T("cmd.ci.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { dryRun := !ciGoForLaunch return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease) }, } -var ciInitCmd = &cobra.Command{ +var ciInitCmd = &cli.Command{ Use: "init", Short: i18n.T("cmd.ci.init.short"), Long: i18n.T("cmd.ci.init.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runCIReleaseInit() }, } -var ciChangelogCmd = &cobra.Command{ +var ciChangelogCmd = &cli.Command{ Use: "changelog", Short: i18n.T("cmd.ci.changelog.short"), Long: i18n.T("cmd.ci.changelog.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runChangelog(changelogFromRef, changelogToRef) }, } -var ciVersionCmd = &cobra.Command{ +var ciVersionCmd = &cli.Command{ Use: "version", Short: i18n.T("cmd.ci.version.short"), Long: i18n.T("cmd.ci.version.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runCIReleaseVersion() }, } diff --git a/pkg/ci/cmd_commands.go b/pkg/ci/cmd_commands.go index 89436b63..bf279c44 100644 --- a/pkg/ci/cmd_commands.go +++ b/pkg/ci/cmd_commands.go @@ -11,7 +11,6 @@ package ci import ( "github.com/host-uk/core/pkg/cli" - "github.com/spf13/cobra" ) func init() { @@ -19,6 +18,6 @@ func init() { } // AddCICommands registers the 'ci' command and all subcommands. -func AddCICommands(root *cobra.Command) { +func AddCICommands(root *cli.Command) { root.AddCommand(ciCmd) } diff --git a/pkg/ci/cmd_init.go b/pkg/ci/cmd_init.go index a5298b12..015c3542 100644 --- a/pkg/ci/cmd_init.go +++ b/pkg/ci/cmd_init.go @@ -2,11 +2,11 @@ package ci import ( "bufio" - "fmt" "os" "path/filepath" "strings" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/release" ) @@ -15,34 +15,34 @@ import ( func runCIReleaseInit() error { projectDir, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } // Check if config already exists if release.ConfigExists(projectDir) { - fmt.Printf("%s %s %s\n", + cli.Print("%s %s %s\n", releaseDimStyle.Render(i18n.Label("note")), i18n.T("cmd.ci.init.config_exists"), release.ConfigPath(projectDir)) reader := bufio.NewReader(os.Stdin) - fmt.Print(i18n.T("cmd.ci.init.overwrite_prompt")) + cli.Print("%s", i18n.T("cmd.ci.init.overwrite_prompt")) response, _ := reader.ReadString('\n') response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { - fmt.Println(i18n.T("common.prompt.abort")) + cli.Text(i18n.T("common.prompt.abort")) return nil } } - fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.init")), i18n.T("cmd.ci.init.creating")) - fmt.Println() + cli.Print("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.init")), i18n.T("cmd.ci.init.creating")) + cli.Line("") reader := bufio.NewReader(os.Stdin) // Project name defaultName := filepath.Base(projectDir) - fmt.Printf("%s [%s]: ", i18n.T("cmd.ci.init.project_name"), defaultName) + cli.Print("%s [%s]: ", i18n.T("cmd.ci.init.project_name"), defaultName) name, _ := reader.ReadString('\n') name = strings.TrimSpace(name) if name == "" { @@ -50,7 +50,7 @@ func runCIReleaseInit() error { } // Repository - fmt.Printf("%s ", i18n.T("cmd.ci.init.github_repo")) + cli.Print("%s ", i18n.T("cmd.ci.init.github_repo")) repo, _ := reader.ReadString('\n') repo = strings.TrimSpace(repo) @@ -61,11 +61,11 @@ func runCIReleaseInit() error { // Write config if err := release.WriteConfig(cfg, projectDir); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.write", "config"), err) + return cli.WrapVerb(err, "write", "config") } - fmt.Println() - fmt.Printf("%s %s %s\n", + cli.Line("") + cli.Print("%s %s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.init.config_written"), release.ConfigPath(projectDir)) diff --git a/pkg/ci/cmd_publish.go b/pkg/ci/cmd_publish.go index 83709f28..1e41bf30 100644 --- a/pkg/ci/cmd_publish.go +++ b/pkg/ci/cmd_publish.go @@ -3,9 +3,9 @@ package ci import ( "context" "errors" - "fmt" "os" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/release" ) @@ -18,13 +18,13 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { // Get current directory projectDir, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } // Load configuration cfg, err := release.LoadConfig(projectDir) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "config"), err) + return cli.WrapVerb(err, "load", "config") } // Apply CLI overrides @@ -45,13 +45,13 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { } // Print header - fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing")) + cli.Print("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing")) if dryRun { - fmt.Printf(" %s\n", releaseDimStyle.Render(i18n.T("cmd.ci.dry_run_hint"))) + cli.Print(" %s\n", releaseDimStyle.Render(i18n.T("cmd.ci.dry_run_hint"))) } else { - fmt.Printf(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch"))) + cli.Print(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch"))) } - fmt.Println() + cli.Line("") // Check for publishers if len(cfg.Publishers) == 0 { @@ -61,19 +61,19 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { // Publish pre-built artifacts rel, err := release.Publish(ctx, cfg, dryRun) if err != nil { - fmt.Printf("%s %v\n", releaseErrorStyle.Render(i18n.Label("error")), err) + cli.Print("%s %v\n", releaseErrorStyle.Render(i18n.Label("error")), err) return err } // Print summary - fmt.Println() - fmt.Printf("%s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed")) - fmt.Printf(" %s %s\n", i18n.Label("version"), releaseValueStyle.Render(rel.Version)) - fmt.Printf(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts)) + cli.Line("") + cli.Print("%s %s\n", releaseSuccessStyle.Render(i18n.T("i18n.done.pass")), i18n.T("cmd.ci.publish_completed")) + cli.Print(" %s %s\n", i18n.Label("version"), releaseValueStyle.Render(rel.Version)) + cli.Print(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts)) if !dryRun { for _, pub := range cfg.Publishers { - fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.published"), releaseValueStyle.Render(pub.Type)) + cli.Print(" %s %s\n", i18n.T("cmd.ci.label.published"), releaseValueStyle.Render(pub.Type)) } } diff --git a/pkg/ci/cmd_version.go b/pkg/ci/cmd_version.go index 8d59f987..f38127c3 100644 --- a/pkg/ci/cmd_version.go +++ b/pkg/ci/cmd_version.go @@ -1,9 +1,9 @@ package ci import ( - "fmt" "os" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/release" ) @@ -12,14 +12,14 @@ import ( func runCIReleaseVersion() error { projectDir, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.WrapVerb(err, "get", "working directory") } version, err := release.DetermineVersion(projectDir) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.determine", "version"), err) + return cli.WrapVerb(err, "determine", "version") } - fmt.Printf("%s %s\n", i18n.Label("version"), releaseValueStyle.Render(version)) + cli.Print("%s %s\n", i18n.Label("version"), releaseValueStyle.Render(version)) return nil } diff --git a/pkg/cli/styles.go b/pkg/cli/styles.go index 49d46bcf..f79809ef 100644 --- a/pkg/cli/styles.go +++ b/pkg/cli/styles.go @@ -20,23 +20,23 @@ import ( // Tailwind colours for consistent theming across the CLI. const ( - ColourBlue50 = lipgloss.Color("#eff6ff") - ColourBlue100 = lipgloss.Color("#dbeafe") - ColourBlue200 = lipgloss.Color("#bfdbfe") - ColourBlue300 = lipgloss.Color("#93c5fd") - ColourBlue400 = lipgloss.Color("#60a5fa") - ColourBlue500 = lipgloss.Color("#3b82f6") - ColourBlue600 = lipgloss.Color("#2563eb") - ColourBlue700 = lipgloss.Color("#1d4ed8") - ColourGreen400 = lipgloss.Color("#4ade80") - ColourGreen500 = lipgloss.Color("#22c55e") - ColourGreen600 = lipgloss.Color("#16a34a") - ColourRed400 = lipgloss.Color("#f87171") - ColourRed500 = lipgloss.Color("#ef4444") - ColourRed600 = lipgloss.Color("#dc2626") - ColourAmber400 = lipgloss.Color("#fbbf24") - ColourAmber500 = lipgloss.Color("#f59e0b") - ColourAmber600 = lipgloss.Color("#d97706") + ColourBlue50 = lipgloss.Color("#eff6ff") + ColourBlue100 = lipgloss.Color("#dbeafe") + ColourBlue200 = lipgloss.Color("#bfdbfe") + ColourBlue300 = lipgloss.Color("#93c5fd") + ColourBlue400 = lipgloss.Color("#60a5fa") + ColourBlue500 = lipgloss.Color("#3b82f6") + ColourBlue600 = lipgloss.Color("#2563eb") + ColourBlue700 = lipgloss.Color("#1d4ed8") + ColourGreen400 = lipgloss.Color("#4ade80") + ColourGreen500 = lipgloss.Color("#22c55e") + ColourGreen600 = lipgloss.Color("#16a34a") + ColourRed400 = lipgloss.Color("#f87171") + ColourRed500 = lipgloss.Color("#ef4444") + ColourRed600 = lipgloss.Color("#dc2626") + ColourAmber400 = lipgloss.Color("#fbbf24") + ColourAmber500 = lipgloss.Color("#f59e0b") + ColourAmber600 = lipgloss.Color("#d97706") ColourOrange500 = lipgloss.Color("#f97316") ColourYellow500 = lipgloss.Color("#eab308") ColourEmerald500 = lipgloss.Color("#10b981") @@ -45,16 +45,16 @@ const ( ColourViolet500 = lipgloss.Color("#8b5cf6") ColourIndigo500 = lipgloss.Color("#6366f1") ColourCyan500 = lipgloss.Color("#06b6d4") - ColourGray50 = lipgloss.Color("#f9fafb") - ColourGray100 = lipgloss.Color("#f3f4f6") - ColourGray200 = lipgloss.Color("#e5e7eb") - ColourGray300 = lipgloss.Color("#d1d5db") - ColourGray400 = lipgloss.Color("#9ca3af") - ColourGray500 = lipgloss.Color("#6b7280") - ColourGray600 = lipgloss.Color("#4b5563") - ColourGray700 = lipgloss.Color("#374151") - ColourGray800 = lipgloss.Color("#1f2937") - ColourGray900 = lipgloss.Color("#111827") + ColourGray50 = lipgloss.Color("#f9fafb") + ColourGray100 = lipgloss.Color("#f3f4f6") + ColourGray200 = lipgloss.Color("#e5e7eb") + ColourGray300 = lipgloss.Color("#d1d5db") + ColourGray400 = lipgloss.Color("#9ca3af") + ColourGray500 = lipgloss.Color("#6b7280") + ColourGray600 = lipgloss.Color("#4b5563") + ColourGray700 = lipgloss.Color("#374151") + ColourGray800 = lipgloss.Color("#1f2937") + ColourGray900 = lipgloss.Color("#111827") ) // ───────────────────────────────────────────────────────────────────────────── @@ -489,8 +489,8 @@ func Path(p string) string { return CodeStyle.Render(p) } -// Command renders a command in code style. -func Command(cmd string) string { +// CommandStr renders a command string in code style. +func CommandStr(cmd string) string { return CodeStyle.Render(cmd) } diff --git a/pkg/dev/cmd_api.go b/pkg/dev/cmd_api.go index b215ac61..559489f9 100644 --- a/pkg/dev/cmd_api.go +++ b/pkg/dev/cmd_api.go @@ -1,14 +1,14 @@ package dev import ( + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // addAPICommands adds the 'api' command and its subcommands to the given parent command. -func addAPICommands(parent *cobra.Command) { +func addAPICommands(parent *cli.Command) { // Create the 'api' command - apiCmd := &cobra.Command{ + apiCmd := &cli.Command{ Use: "api", Short: i18n.T("cmd.dev.api.short"), } diff --git a/pkg/dev/cmd_ci.go b/pkg/dev/cmd_ci.go index a07a8684..39554cac 100644 --- a/pkg/dev/cmd_ci.go +++ b/pkg/dev/cmd_ci.go @@ -3,7 +3,6 @@ package dev import ( "encoding/json" "errors" - "fmt" "os" "os/exec" "strings" @@ -12,7 +11,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // CI-specific styles (aliases to shared) @@ -45,12 +43,12 @@ var ( ) // addCICommand adds the 'ci' command to the given parent command. -func addCICommand(parent *cobra.Command) { - ciCmd := &cobra.Command{ +func addCICommand(parent *cli.Command) { + ciCmd := &cli.Command{ Use: "ci", Short: i18n.T("cmd.dev.ci.short"), Long: i18n.T("cmd.dev.ci.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { branch := ciBranch if branch == "" { branch = "main" @@ -79,20 +77,20 @@ func runCI(registryPath string, branch string, failedOnly bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } } } @@ -104,15 +102,15 @@ func runCI(registryPath string, branch string, failedOnly bool) error { repoList := reg.List() for i, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) - fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.check")), i+1, len(repoList), repo.Name) + repoFullName := cli.Sprintf("%s/%s", reg.Org, repo.Name) + cli.Print("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.check")), i+1, len(repoList), repo.Name) runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch) if err != nil { if strings.Contains(err.Error(), "no workflows") { noCI = append(noCI, repo.Name) } else { - fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err)) + fetchErrors = append(fetchErrors, cli.Wrap(err, repo.Name)) } continue } @@ -124,7 +122,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error { noCI = append(noCI, repo.Name) } } - fmt.Print("\033[2K\r") // Clear progress line + cli.Print("\033[2K\r") // Clear progress line // Count by status var success, failed, pending, other int @@ -146,22 +144,22 @@ func runCI(registryPath string, branch string, failedOnly bool) error { } // Print summary - fmt.Println() - fmt.Printf("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)})) + cli.Line("") + cli.Print("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)})) if success > 0 { - fmt.Printf(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success}))) + cli.Print(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success}))) } if failed > 0 { - fmt.Printf(" * %s", ciFailureStyle.Render(i18n.T("cmd.dev.ci.failing", map[string]interface{}{"Count": failed}))) + cli.Print(" * %s", ciFailureStyle.Render(i18n.T("cmd.dev.ci.failing", map[string]interface{}{"Count": failed}))) } if pending > 0 { - fmt.Printf(" * %s", ciPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending}))) + cli.Print(" * %s", ciPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending}))) } if len(noCI) > 0 { - fmt.Printf(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)}))) + cli.Print(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)}))) } - fmt.Println() - fmt.Println() + cli.Line("") + cli.Line("") // Filter if needed displayRuns := allRuns @@ -181,9 +179,9 @@ func runCI(registryPath string, branch string, failedOnly bool) error { // Print errors if len(fetchErrors) > 0 { - fmt.Println() + cli.Line("") for _, err := range fetchErrors { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("error")), err) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) } } @@ -204,7 +202,7 @@ func fetchWorkflowRuns(repoFullName, repoName string, branch string) ([]Workflow if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { stderr := string(exitErr.Stderr) - return nil, fmt.Errorf("%s", strings.TrimSpace(stderr)) + return nil, cli.Err("%s", strings.TrimSpace(stderr)) } return nil, err } @@ -252,7 +250,7 @@ func printWorkflowRun(run WorkflowRun) { // Age age := cli.FormatAge(run.UpdatedAt) - fmt.Printf(" %s %-18s %-22s %s\n", + cli.Print(" %s %-18s %-22s %s\n", status, repoNameStyle.Render(run.RepoName), dimStyle.Render(workflowName), diff --git a/pkg/dev/cmd_commit.go b/pkg/dev/cmd_commit.go index a50067e5..554ed6db 100644 --- a/pkg/dev/cmd_commit.go +++ b/pkg/dev/cmd_commit.go @@ -2,7 +2,6 @@ package dev import ( "context" - "fmt" "os" "path/filepath" @@ -10,7 +9,6 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Commit command flags @@ -20,12 +18,12 @@ var ( ) // addCommitCommand adds the 'commit' command to the given parent command. -func addCommitCommand(parent *cobra.Command) { - commitCmd := &cobra.Command{ +func addCommitCommand(parent *cli.Command) { + commitCmd := &cli.Command{ Use: "commit", Short: i18n.T("cmd.dev.commit.short"), Long: i18n.T("cmd.dev.commit.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runCommit(commitRegistryPath, commitAll) }, } @@ -52,24 +50,24 @@ func runCommit(registryPath string, all bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { // Fallback: scan current directory for repos reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) registryPath = cwd } } @@ -86,7 +84,7 @@ func runCommit(registryPath string, all bool) error { } if len(paths) == 0 { - fmt.Println(i18n.T("cmd.dev.no_git_repos")) + cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } @@ -105,58 +103,58 @@ func runCommit(registryPath string, all bool) error { } if len(dirtyRepos) == 0 { - fmt.Println(i18n.T("cmd.dev.no_changes")) + cli.Text(i18n.T("cmd.dev.no_changes")) return nil } // Show dirty repos - fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.repos_with_changes", map[string]interface{}{"Count": len(dirtyRepos)})) + cli.Print("\n%s\n\n", i18n.T("cmd.dev.repos_with_changes", map[string]interface{}{"Count": len(dirtyRepos)})) for _, s := range dirtyRepos { - fmt.Printf(" %s: ", repoNameStyle.Render(s.Name)) + cli.Print(" %s: ", repoNameStyle.Render(s.Name)) if s.Modified > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) } if s.Untracked > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) } if s.Staged > 0 { - fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) + cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) } - fmt.Println() + cli.Line("") } // Confirm unless --all if !all { - fmt.Println() + cli.Line("") if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) { - fmt.Println(i18n.T("cli.aborted")) + cli.Text(i18n.T("cli.aborted")) return nil } } - fmt.Println() + cli.Line("") // Commit each dirty repo var succeeded, failed int for _, s := range dirtyRepos { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.committing")), s.Name) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.committing")), s.Name) if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil { - fmt.Printf(" %s %s\n", errorStyle.Render("x"), err) + cli.Print(" %s %s\n", errorStyle.Render("x"), err) failed++ } else { - fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) + cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) succeeded++ } - fmt.Println() + cli.Line("") } // Summary - fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.done_succeeded", map[string]interface{}{"Count": succeeded}))) + cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.done_succeeded", map[string]interface{}{"Count": succeeded}))) if failed > 0 { - fmt.Printf(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) + cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) } - fmt.Println() + cli.Line("") return nil } @@ -182,44 +180,44 @@ func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error { if len(statuses) > 0 && statuses[0].Error != nil { return statuses[0].Error } - return fmt.Errorf("failed to get repo status") + return cli.Err("failed to get repo status") } s := statuses[0] if !s.IsDirty() { - fmt.Println(i18n.T("cmd.dev.no_changes")) + cli.Text(i18n.T("cmd.dev.no_changes")) return nil } // Show status - fmt.Printf("%s: ", repoNameStyle.Render(s.Name)) + cli.Print("%s: ", repoNameStyle.Render(s.Name)) if s.Modified > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) } if s.Untracked > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) } if s.Staged > 0 { - fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) + cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) } - fmt.Println() + cli.Line("") // Confirm unless --all if !all { - fmt.Println() + cli.Line("") if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) { - fmt.Println(i18n.T("cli.aborted")) + cli.Text(i18n.T("cli.aborted")) return nil } } - fmt.Println() + cli.Line("") // Commit if err := claudeCommit(ctx, repoPath, repoName, ""); err != nil { - fmt.Printf(" %s %s\n", errorStyle.Render("x"), err) + cli.Print(" %s %s\n", errorStyle.Render("x"), err) return err } - fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) + cli.Print(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed")) return nil } diff --git a/pkg/dev/cmd_dev.go b/pkg/dev/cmd_dev.go index 799c5e9b..411df3f9 100644 --- a/pkg/dev/cmd_dev.go +++ b/pkg/dev/cmd_dev.go @@ -31,7 +31,6 @@ package dev import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) func init() { @@ -57,8 +56,8 @@ var ( ) // AddDevCommands registers the 'dev' command and all subcommands. -func AddDevCommands(root *cobra.Command) { - devCmd := &cobra.Command{ +func AddDevCommands(root *cli.Command) { + devCmd := &cli.Command{ Use: "dev", Short: i18n.T("cmd.dev.short"), Long: i18n.T("cmd.dev.long"), diff --git a/pkg/dev/cmd_health.go b/pkg/dev/cmd_health.go index 2f42c999..693aaaba 100644 --- a/pkg/dev/cmd_health.go +++ b/pkg/dev/cmd_health.go @@ -2,7 +2,6 @@ package dev import ( "context" - "fmt" "os" "sort" @@ -10,7 +9,6 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Health command flags @@ -20,12 +18,12 @@ var ( ) // addHealthCommand adds the 'health' command to the given parent command. -func addHealthCommand(parent *cobra.Command) { - healthCmd := &cobra.Command{ +func addHealthCommand(parent *cli.Command) { + healthCmd := &cli.Command{ Use: "health", Short: i18n.T("cmd.dev.health.short"), Long: i18n.T("cmd.dev.health.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runHealth(healthRegistryPath, healthVerbose) }, } @@ -46,21 +44,21 @@ func runHealth(registryPath string, verbose bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } } } @@ -77,7 +75,7 @@ func runHealth(registryPath string, verbose bool) error { } if len(paths) == 0 { - fmt.Println(i18n.T("cmd.dev.no_git_repos")) + cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } @@ -118,25 +116,25 @@ func runHealth(registryPath string, verbose bool) error { } // Print summary line - fmt.Println() + cli.Line("") printHealthSummary(totalRepos, dirtyRepos, aheadRepos, behindRepos, errorRepos) - fmt.Println() + cli.Line("") // Verbose output if verbose { if len(dirtyRepos) > 0 { - fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.dirty_label")), formatRepoList(dirtyRepos)) + cli.Print("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.dirty_label")), formatRepoList(dirtyRepos)) } if len(aheadRepos) > 0 { - fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.dev.health.ahead_label")), formatRepoList(aheadRepos)) + cli.Print("%s %s\n", successStyle.Render(i18n.T("cmd.dev.health.ahead_label")), formatRepoList(aheadRepos)) } if len(behindRepos) > 0 { - fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.behind_label")), formatRepoList(behindRepos)) + cli.Print("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.behind_label")), formatRepoList(behindRepos)) } if len(errorRepos) > 0 { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos)) + cli.Print("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos)) } - fmt.Println() + cli.Line("") } return nil @@ -173,7 +171,7 @@ func printHealthSummary(total int, dirty, ahead, behind, errors []string) { parts = append(parts, cli.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), cli.ErrorStyle)) } - fmt.Println(cli.StatusLine(parts...)) + cli.Text(cli.StatusLine(parts...)) } func formatRepoList(reposList []string) string { diff --git a/pkg/dev/cmd_impact.go b/pkg/dev/cmd_impact.go index b3483229..373b50cc 100644 --- a/pkg/dev/cmd_impact.go +++ b/pkg/dev/cmd_impact.go @@ -2,13 +2,11 @@ package dev import ( "errors" - "fmt" "sort" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Impact-specific styles (aliases to shared) @@ -22,13 +20,13 @@ var ( var impactRegistryPath string // addImpactCommand adds the 'impact' command to the given parent command. -func addImpactCommand(parent *cobra.Command) { - impactCmd := &cobra.Command{ +func addImpactCommand(parent *cli.Command) { + impactCmd := &cli.Command{ Use: "impact ", Short: i18n.T("cmd.dev.impact.short"), Long: i18n.T("cmd.dev.impact.long"), - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + Args: cli.ExactArgs(1), + RunE: func(cmd *cli.Command, args []string) error { return runImpact(impactRegistryPath, args[0]) }, } @@ -46,14 +44,14 @@ func runImpact(registryPath string, repoName string) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { return errors.New(i18n.T("cmd.dev.impact.requires_registry")) @@ -91,21 +89,21 @@ func runImpact(registryPath string, repoName string) error { sort.Strings(indirect) // Print results - fmt.Println() - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName)) + cli.Line("") + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName)) if repo.Description != "" { - fmt.Printf("%s\n", dimStyle.Render(repo.Description)) + cli.Print("%s\n", dimStyle.Render(repo.Description)) } - fmt.Println() + cli.Line("") if len(allAffected) == 0 { - fmt.Printf("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName})) + cli.Print("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName})) return nil } // Direct dependents if len(direct) > 0 { - fmt.Printf("%s %s\n", + cli.Print("%s %s\n", impactDirectStyle.Render("*"), i18n.T("cmd.dev.impact.direct_dependents", map[string]interface{}{"Count": len(direct)}), ) @@ -115,14 +113,14 @@ func runImpact(registryPath string, repoName string) error { if r != nil && r.Description != "" { desc = dimStyle.Render(" - " + cli.Truncate(r.Description, 40)) } - fmt.Printf(" %s%s\n", d, desc) + cli.Print(" %s%s\n", d, desc) } - fmt.Println() + cli.Line("") } // Indirect dependents if len(indirect) > 0 { - fmt.Printf("%s %s\n", + cli.Print("%s %s\n", impactIndirectStyle.Render("o"), i18n.T("cmd.dev.impact.transitive_dependents", map[string]interface{}{"Count": len(indirect)}), ) @@ -132,13 +130,13 @@ func runImpact(registryPath string, repoName string) error { if r != nil && r.Description != "" { desc = dimStyle.Render(" - " + cli.Truncate(r.Description, 40)) } - fmt.Printf(" %s%s\n", d, desc) + cli.Print(" %s%s\n", d, desc) } - fmt.Println() + cli.Line("") } // Summary - fmt.Printf("%s %s\n", + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.dev.impact.changes_affect", map[string]interface{}{ "Repo": repoNameStyle.Render(repoName), diff --git a/pkg/dev/cmd_issues.go b/pkg/dev/cmd_issues.go index 094e9088..425fab33 100644 --- a/pkg/dev/cmd_issues.go +++ b/pkg/dev/cmd_issues.go @@ -3,7 +3,6 @@ package dev import ( "encoding/json" "errors" - "fmt" "os" "os/exec" "sort" @@ -13,7 +12,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Issue-specific styles (aliases to shared) @@ -59,12 +57,12 @@ var ( ) // addIssuesCommand adds the 'issues' command to the given parent command. -func addIssuesCommand(parent *cobra.Command) { - issuesCmd := &cobra.Command{ +func addIssuesCommand(parent *cli.Command) { + issuesCmd := &cli.Command{ Use: "issues", Short: i18n.T("cmd.dev.issues.short"), Long: i18n.T("cmd.dev.issues.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { limit := issuesLimit if limit == 0 { limit = 10 @@ -93,21 +91,21 @@ func runIssues(registryPath string, limit int, assignee string) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } } } @@ -118,17 +116,17 @@ func runIssues(registryPath string, limit int, assignee string) error { repoList := reg.List() for i, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) - fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.fetch")), i+1, len(repoList), repo.Name) + repoFullName := cli.Sprintf("%s/%s", reg.Org, repo.Name) + cli.Print("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.fetch")), i+1, len(repoList), repo.Name) issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee) if err != nil { - fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err)) + fetchErrors = append(fetchErrors, cli.Wrap(err, repo.Name)) continue } allIssues = append(allIssues, issues...) } - fmt.Print("\033[2K\r") // Clear progress line + cli.Print("\033[2K\r") // Clear progress line // Sort by created date (newest first) sort.Slice(allIssues, func(i, j int) bool { @@ -137,11 +135,11 @@ func runIssues(registryPath string, limit int, assignee string) error { // Print issues if len(allIssues) == 0 { - fmt.Println(i18n.T("cmd.dev.issues.no_issues")) + cli.Text(i18n.T("cmd.dev.issues.no_issues")) return nil } - fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.issues.open_issues", map[string]interface{}{"Count": len(allIssues)})) + cli.Print("\n%s\n\n", i18n.T("cmd.dev.issues.open_issues", map[string]interface{}{"Count": len(allIssues)})) for _, issue := range allIssues { printIssue(issue) @@ -149,9 +147,9 @@ func runIssues(registryPath string, limit int, assignee string) error { // Print any errors if len(fetchErrors) > 0 { - fmt.Println() + cli.Line("") for _, err := range fetchErrors { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("error")), err) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) } } @@ -163,7 +161,7 @@ func fetchIssues(repoFullName, repoName string, limit int, assignee string) ([]G "issue", "list", "--repo", repoFullName, "--state", "open", - "--limit", fmt.Sprintf("%d", limit), + "--limit", cli.Sprintf("%d", limit), "--json", "number,title,state,createdAt,author,assignees,labels,url", } @@ -180,7 +178,7 @@ func fetchIssues(repoFullName, repoName string, limit int, assignee string) ([]G if strings.Contains(stderr, "no issues") || strings.Contains(stderr, "Could not resolve") { return nil, nil } - return nil, fmt.Errorf("%s", stderr) + return nil, cli.Err("%s", stderr) } return nil, err } @@ -200,11 +198,11 @@ func fetchIssues(repoFullName, repoName string, limit int, assignee string) ([]G func printIssue(issue GitHubIssue) { // #42 [core-bio] Fix avatar upload - num := issueNumberStyle.Render(fmt.Sprintf("#%d", issue.Number)) - repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", issue.RepoName)) + num := issueNumberStyle.Render(cli.Sprintf("#%d", issue.Number)) + repo := issueRepoStyle.Render(cli.Sprintf("[%s]", issue.RepoName)) title := issueTitleStyle.Render(cli.Truncate(issue.Title, 60)) - line := fmt.Sprintf(" %s %s %s", num, repo, title) + line := cli.Sprintf(" %s %s %s", num, repo, title) // Add labels if any if len(issue.Labels.Nodes) > 0 { @@ -228,5 +226,5 @@ func printIssue(issue GitHubIssue) { age := cli.FormatAge(issue.CreatedAt) line += " " + issueAgeStyle.Render(age) - fmt.Println(line) + cli.Text(line) } diff --git a/pkg/dev/cmd_pull.go b/pkg/dev/cmd_pull.go index ec254fe8..4bfca5d8 100644 --- a/pkg/dev/cmd_pull.go +++ b/pkg/dev/cmd_pull.go @@ -2,14 +2,13 @@ package dev import ( "context" - "fmt" "os" "os/exec" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Pull command flags @@ -19,12 +18,12 @@ var ( ) // addPullCommand adds the 'pull' command to the given parent command. -func addPullCommand(parent *cobra.Command) { - pullCmd := &cobra.Command{ +func addPullCommand(parent *cli.Command) { + pullCmd := &cli.Command{ Use: "pull", Short: i18n.T("cmd.dev.pull.short"), Long: i18n.T("cmd.dev.pull.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runPull(pullRegistryPath, pullAll) }, } @@ -45,25 +44,25 @@ func runPull(registryPath string, all bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) } } @@ -79,7 +78,7 @@ func runPull(registryPath string, all bool) error { } if len(paths) == 0 { - fmt.Println(i18n.T("cmd.dev.no_git_repos")) + cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } @@ -101,46 +100,46 @@ func runPull(registryPath string, all bool) error { } if len(toPull) == 0 { - fmt.Println(i18n.T("cmd.dev.pull.all_up_to_date")) + cli.Text(i18n.T("cmd.dev.pull.all_up_to_date")) return nil } // Show what we're pulling if all { - fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.pulling_repos", map[string]interface{}{"Count": len(toPull)})) + cli.Print("\n%s\n\n", i18n.T("cmd.dev.pull.pulling_repos", map[string]interface{}{"Count": len(toPull)})) } else { - fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.repos_behind", map[string]interface{}{"Count": len(toPull)})) + cli.Print("\n%s\n\n", i18n.T("cmd.dev.pull.repos_behind", map[string]interface{}{"Count": len(toPull)})) for _, s := range toPull { - fmt.Printf(" %s: %s\n", + cli.Print(" %s: %s\n", repoNameStyle.Render(s.Name), dimStyle.Render(i18n.T("cmd.dev.pull.commits_behind", map[string]interface{}{"Count": s.Behind})), ) } - fmt.Println() + cli.Line("") } // Pull each repo var succeeded, failed int for _, s := range toPull { - fmt.Printf(" %s %s... ", dimStyle.Render(i18n.T("cmd.dev.pull.pulling")), s.Name) + cli.Print(" %s %s... ", dimStyle.Render(i18n.T("cmd.dev.pull.pulling")), s.Name) err := gitPull(ctx, s.Path) if err != nil { - fmt.Printf("%s\n", errorStyle.Render("x "+err.Error())) + cli.Print("%s\n", errorStyle.Render("x "+err.Error())) failed++ } else { - fmt.Printf("%s\n", successStyle.Render("v")) + cli.Print("%s\n", successStyle.Render("v")) succeeded++ } } // Summary - fmt.Println() - fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded}))) + cli.Line("") + cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded}))) if failed > 0 { - fmt.Printf(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) + cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) } - fmt.Println() + cli.Line("") return nil } @@ -150,7 +149,7 @@ func gitPull(ctx context.Context, path string) error { cmd.Dir = path output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s", string(output)) + return cli.Err("%s", string(output)) } return nil } diff --git a/pkg/dev/cmd_push.go b/pkg/dev/cmd_push.go index ec53f5f5..ed0a1ed5 100644 --- a/pkg/dev/cmd_push.go +++ b/pkg/dev/cmd_push.go @@ -2,7 +2,6 @@ package dev import ( "context" - "fmt" "os" "path/filepath" @@ -10,7 +9,6 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Push command flags @@ -20,12 +18,12 @@ var ( ) // addPushCommand adds the 'push' command to the given parent command. -func addPushCommand(parent *cobra.Command) { - pushCmd := &cobra.Command{ +func addPushCommand(parent *cli.Command) { + pushCmd := &cli.Command{ Use: "push", Short: i18n.T("cmd.dev.push.short"), Long: i18n.T("cmd.dev.push.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runPush(pushRegistryPath, pushForce) }, } @@ -52,24 +50,24 @@ func runPush(registryPath string, force bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { // Fallback: scan current directory for repos reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) } } @@ -85,7 +83,7 @@ func runPush(registryPath string, force bool) error { } if len(paths) == 0 { - fmt.Println(i18n.T("cmd.dev.no_git_repos")) + cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } @@ -104,15 +102,15 @@ func runPush(registryPath string, force bool) error { } if len(aheadRepos) == 0 { - fmt.Println(i18n.T("cmd.dev.push.all_up_to_date")) + cli.Text(i18n.T("cmd.dev.push.all_up_to_date")) return nil } // Show repos to push - fmt.Printf("\n%s\n\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)})) + cli.Print("\n%s\n\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)})) totalCommits := 0 for _, s := range aheadRepos { - fmt.Printf(" %s: %s\n", + cli.Print(" %s: %s\n", repoNameStyle.Render(s.Name), aheadStyle.Render(i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead})), ) @@ -121,14 +119,14 @@ func runPush(registryPath string, force bool) error { // Confirm unless --force if !force { - fmt.Println() + cli.Line("") if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) { - fmt.Println(i18n.T("cli.aborted")) + cli.Text(i18n.T("cli.aborted")) return nil } } - fmt.Println() + cli.Line("") // Push sequentially (SSH passphrase needs interaction) var pushPaths []string @@ -143,15 +141,15 @@ func runPush(registryPath string, force bool) error { for _, r := range results { if r.Success { - fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name) + cli.Print(" %s %s\n", successStyle.Render("v"), r.Name) succeeded++ } else { // Check if this is a non-fast-forward error (diverged branch) if git.IsNonFastForward(r.Error) { - fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), r.Name, i18n.T("cmd.dev.push.diverged")) + cli.Print(" %s %s: %s\n", warningStyle.Render("!"), r.Name, i18n.T("cmd.dev.push.diverged")) divergedRepos = append(divergedRepos, r) } else { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), r.Name, r.Error) } failed++ } @@ -159,22 +157,22 @@ func runPush(registryPath string, force bool) error { // Handle diverged repos - offer to pull and retry if len(divergedRepos) > 0 { - fmt.Println() - fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help")) + cli.Line("") + cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { - fmt.Println() + cli.Line("") for _, r := range divergedRepos { - fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), r.Name) + cli.Print(" %s %s...\n", dimStyle.Render("↓"), r.Name) if err := git.Pull(ctx, r.Path); err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) continue } - fmt.Printf(" %s %s...\n", dimStyle.Render("↑"), r.Name) + cli.Print(" %s %s...\n", dimStyle.Render("↑"), r.Name) if err := git.Push(ctx, r.Path); err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), r.Name, err) continue } - fmt.Printf(" %s %s\n", successStyle.Render("v"), r.Name) + cli.Print(" %s %s\n", successStyle.Render("v"), r.Name) succeeded++ failed-- } @@ -182,12 +180,12 @@ func runPush(registryPath string, force bool) error { } // Summary - fmt.Println() - fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded}))) + cli.Line("") + cli.Print("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded}))) if failed > 0 { - fmt.Printf(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) + cli.Print(", %s", errorStyle.Render(i18n.T("common.count.failed", map[string]interface{}{"Count": failed}))) } - fmt.Println() + cli.Line("") return nil } @@ -203,7 +201,7 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error { }) if len(statuses) == 0 { - return fmt.Errorf("failed to get repo status") + return cli.Err("failed to get repo status") } s := statuses[0] @@ -214,20 +212,20 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error { if !s.HasUnpushed() { // Check if there are uncommitted changes if s.IsDirty() { - fmt.Printf("%s: ", repoNameStyle.Render(s.Name)) + cli.Print("%s: ", repoNameStyle.Render(s.Name)) if s.Modified > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified}))) } if s.Untracked > 0 { - fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) + cli.Print("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked}))) } if s.Staged > 0 { - fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) + cli.Print("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged}))) } - fmt.Println() - fmt.Println() + cli.Line("") + cli.Line("") if cli.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) { - fmt.Println() + cli.Line("") // Use edit-enabled commit if only untracked files (may need .gitignore fix) var err error if s.Modified == 0 && s.Staged == 0 && s.Untracked > 0 { @@ -249,52 +247,52 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error { } return nil } - fmt.Println(i18n.T("cmd.dev.push.all_up_to_date")) + cli.Text(i18n.T("cmd.dev.push.all_up_to_date")) return nil } // Show commits to push - fmt.Printf("%s: %s\n", repoNameStyle.Render(s.Name), + cli.Print("%s: %s\n", repoNameStyle.Render(s.Name), aheadStyle.Render(i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead}))) // Confirm unless --force if !force { - fmt.Println() + cli.Line("") if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) { - fmt.Println(i18n.T("cli.aborted")) + cli.Text(i18n.T("cli.aborted")) return nil } } - fmt.Println() + cli.Line("") // Push err := git.Push(ctx, repoPath) if err != nil { if git.IsNonFastForward(err) { - fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged")) - fmt.Println() - fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help")) + cli.Print(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged")) + cli.Line("") + cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { - fmt.Println() - fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), repoName) + cli.Line("") + cli.Print(" %s %s...\n", dimStyle.Render("↓"), repoName) if pullErr := git.Pull(ctx, repoPath); pullErr != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, pullErr) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), repoName, pullErr) return pullErr } - fmt.Printf(" %s %s...\n", dimStyle.Render("↑"), repoName) + cli.Print(" %s %s...\n", dimStyle.Render("↑"), repoName) if pushErr := git.Push(ctx, repoPath); pushErr != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, pushErr) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), repoName, pushErr) return pushErr } - fmt.Printf(" %s %s\n", successStyle.Render("v"), repoName) + cli.Print(" %s %s\n", successStyle.Render("v"), repoName) return nil } } - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), repoName, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), repoName, err) return err } - fmt.Printf(" %s %s\n", successStyle.Render("v"), repoName) + cli.Print(" %s %s\n", successStyle.Render("v"), repoName) return nil } diff --git a/pkg/dev/cmd_reviews.go b/pkg/dev/cmd_reviews.go index 7a4ff472..3ba51d74 100644 --- a/pkg/dev/cmd_reviews.go +++ b/pkg/dev/cmd_reviews.go @@ -3,7 +3,6 @@ package dev import ( "encoding/json" "errors" - "fmt" "os" "os/exec" "sort" @@ -13,7 +12,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // PR-specific styles (aliases to shared) @@ -60,12 +58,12 @@ var ( ) // addReviewsCommand adds the 'reviews' command to the given parent command. -func addReviewsCommand(parent *cobra.Command) { - reviewsCmd := &cobra.Command{ +func addReviewsCommand(parent *cli.Command) { + reviewsCmd := &cli.Command{ Use: "reviews", Short: i18n.T("cmd.dev.reviews.short"), Long: i18n.T("cmd.dev.reviews.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll) }, } @@ -90,21 +88,21 @@ func runReviews(registryPath string, author string, showAll bool) error { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return fmt.Errorf("failed to load registry: %w", err) + return cli.Wrap(err, "failed to load registry") } } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return fmt.Errorf("failed to scan directory: %w", err) + return cli.Wrap(err, "failed to scan directory") } } } @@ -115,12 +113,12 @@ func runReviews(registryPath string, author string, showAll bool) error { repoList := reg.List() for i, repo := range repoList { - repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) - fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.fetch")), i+1, len(repoList), repo.Name) + repoFullName := cli.Sprintf("%s/%s", reg.Org, repo.Name) + cli.Print("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("i18n.progress.fetch")), i+1, len(repoList), repo.Name) prs, err := fetchPRs(repoFullName, repo.Name, author) if err != nil { - fetchErrors = append(fetchErrors, fmt.Errorf("%s: %w", repo.Name, err)) + fetchErrors = append(fetchErrors, cli.Wrap(err, repo.Name)) continue } @@ -132,7 +130,7 @@ func runReviews(registryPath string, author string, showAll bool) error { allPRs = append(allPRs, pr) } } - fmt.Print("\033[2K\r") // Clear progress line + cli.Print("\033[2K\r") // Clear progress line // Sort: pending review first, then by date sort.Slice(allPRs, func(i, j int) bool { @@ -147,7 +145,7 @@ func runReviews(registryPath string, author string, showAll bool) error { // Print PRs if len(allPRs) == 0 { - fmt.Println(i18n.T("cmd.dev.reviews.no_prs")) + cli.Text(i18n.T("cmd.dev.reviews.no_prs")) return nil } @@ -164,19 +162,19 @@ func runReviews(registryPath string, author string, showAll bool) error { } } - fmt.Println() - fmt.Printf("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)})) + cli.Line("") + cli.Print("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)})) if pending > 0 { - fmt.Printf(" * %s", prPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending}))) + cli.Print(" * %s", prPendingStyle.Render(i18n.T("common.count.pending", map[string]interface{}{"Count": pending}))) } if approved > 0 { - fmt.Printf(" * %s", prApprovedStyle.Render(i18n.T("cmd.dev.reviews.approved", map[string]interface{}{"Count": approved}))) + cli.Print(" * %s", prApprovedStyle.Render(i18n.T("cmd.dev.reviews.approved", map[string]interface{}{"Count": approved}))) } if changesRequested > 0 { - fmt.Printf(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested}))) + cli.Print(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested}))) } - fmt.Println() - fmt.Println() + cli.Line("") + cli.Line("") for _, pr := range allPRs { printPR(pr) @@ -184,9 +182,9 @@ func runReviews(registryPath string, author string, showAll bool) error { // Print any errors if len(fetchErrors) > 0 { - fmt.Println() + cli.Line("") for _, err := range fetchErrors { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("error")), err) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), err) } } @@ -213,7 +211,7 @@ func fetchPRs(repoFullName, repoName string, author string) ([]GitHubPR, error) if strings.Contains(stderr, "no pull requests") || strings.Contains(stderr, "Could not resolve") { return nil, nil } - return nil, fmt.Errorf("%s", stderr) + return nil, cli.Err("%s", stderr) } return nil, err } @@ -233,8 +231,8 @@ func fetchPRs(repoFullName, repoName string, author string) ([]GitHubPR, error) func printPR(pr GitHubPR) { // #12 [core-php] Webhook validation - num := prNumberStyle.Render(fmt.Sprintf("#%d", pr.Number)) - repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", pr.RepoName)) + num := prNumberStyle.Render(cli.Sprintf("#%d", pr.Number)) + repo := issueRepoStyle.Render(cli.Sprintf("[%s]", pr.RepoName)) title := prTitleStyle.Render(cli.Truncate(pr.Title, 50)) author := prAuthorStyle.Render("@" + pr.Author.Login) @@ -257,5 +255,5 @@ func printPR(pr GitHubPR) { age := cli.FormatAge(pr.CreatedAt) - fmt.Printf(" %s %s %s%s %s %s %s\n", num, repo, title, draft, author, status, issueAgeStyle.Render(age)) + cli.Print(" %s %s %s%s %s %s %s\n", num, repo, title, draft, author, status, issueAgeStyle.Render(age)) } diff --git a/pkg/dev/cmd_sync.go b/pkg/dev/cmd_sync.go index 2c96c523..87a0a969 100644 --- a/pkg/dev/cmd_sync.go +++ b/pkg/dev/cmd_sync.go @@ -2,7 +2,6 @@ package dev import ( "bytes" - "fmt" "go/ast" "go/parser" "go/token" @@ -10,23 +9,23 @@ import ( "path/filepath" "text/template" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" ) // addSyncCommand adds the 'sync' command to the given parent command. -func addSyncCommand(parent *cobra.Command) { - syncCmd := &cobra.Command{ +func addSyncCommand(parent *cli.Command) { + syncCmd := &cli.Command{ Use: "sync", Short: i18n.T("cmd.dev.sync.short"), Long: i18n.T("cmd.dev.sync.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { if err := runSync(); err != nil { - return fmt.Errorf("%s %w", i18n.Label("error"), err) + return cli.Wrap(err, i18n.Label("error")) } - fmt.Println(i18n.T("i18n.done.sync", "public APIs")) + cli.Text(i18n.T("i18n.done.sync", "public APIs")) return nil }, } @@ -43,7 +42,7 @@ func runSync() error { pkgDir := "pkg" internalDirs, err := os.ReadDir(pkgDir) if err != nil { - return fmt.Errorf("failed to read pkg directory: %w", err) + return cli.Wrap(err, "failed to read pkg directory") } for _, dir := range internalDirs { @@ -62,11 +61,11 @@ func runSync() error { symbols, err := getExportedSymbols(internalFile) if err != nil { - return fmt.Errorf("error getting symbols for service '%s': %w", serviceName, err) + return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName)) } if err := generatePublicAPIFile(publicDir, publicFile, serviceName, symbols); err != nil { - return fmt.Errorf("error generating public API file for service '%s': %w", serviceName, err) + return cli.Wrap(err, cli.Sprintf("error generating public API file for service '%s'", serviceName)) } } diff --git a/pkg/dev/cmd_vm.go b/pkg/dev/cmd_vm.go index 156320b3..e2190132 100644 --- a/pkg/dev/cmd_vm.go +++ b/pkg/dev/cmd_vm.go @@ -3,18 +3,17 @@ package dev import ( "context" "errors" - "fmt" "os" "time" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/devops" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // addVMCommands adds the dev environment VM commands to the dev parent command. // These are added as direct subcommands: core dev install, core dev boot, etc. -func addVMCommands(parent *cobra.Command) { +func addVMCommands(parent *cli.Command) { addVMInstallCommand(parent) addVMBootCommand(parent) addVMStopCommand(parent) @@ -27,12 +26,12 @@ func addVMCommands(parent *cobra.Command) { } // addVMInstallCommand adds the 'dev install' command. -func addVMInstallCommand(parent *cobra.Command) { - installCmd := &cobra.Command{ +func addVMInstallCommand(parent *cli.Command) { + installCmd := &cli.Command{ Use: "install", Short: i18n.T("cmd.dev.vm.install.short"), Long: i18n.T("cmd.dev.vm.install.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMInstall() }, } @@ -47,16 +46,16 @@ func runVMInstall() error { } if d.IsInstalled() { - fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.already_installed"))) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")})) + cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.already_installed"))) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")})) return nil } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("image")), devops.ImageName()) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.downloading")) - fmt.Println() + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("image")), devops.ImageName()) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.downloading")) + cli.Line("") ctx := context.Background() start := time.Now() @@ -66,23 +65,23 @@ func runVMInstall() error { if total > 0 { pct := int(float64(downloaded) / float64(total) * 100) if pct != int(float64(lastProgress)/float64(total)*100) { - fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) + cli.Print("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) lastProgress = downloaded } } }) - fmt.Println() // Clear progress line + cli.Line("") // Clear progress line if err != nil { - return fmt.Errorf("install failed: %w", err) + return cli.Wrap(err, "install failed") } elapsed := time.Since(start).Round(time.Second) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed})) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed})) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) return nil } @@ -95,12 +94,12 @@ var ( ) // addVMBootCommand adds the 'devops boot' command. -func addVMBootCommand(parent *cobra.Command) { - bootCmd := &cobra.Command{ +func addVMBootCommand(parent *cli.Command) { + bootCmd := &cli.Command{ Use: "boot", Short: i18n.T("cmd.dev.vm.boot.short"), Long: i18n.T("cmd.dev.vm.boot.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh) }, } @@ -131,31 +130,31 @@ func runVMBoot(memory, cpus int, fresh bool) error { } opts.Fresh = fresh - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs})) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.booting")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs})) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.booting")) ctx := context.Background() if err := d.Boot(ctx, opts); err != nil { return err } - fmt.Println() - fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.running"))) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")})) - fmt.Printf("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222")) + cli.Line("") + cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.running"))) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")})) + cli.Print("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222")) return nil } // addVMStopCommand adds the 'devops stop' command. -func addVMStopCommand(parent *cobra.Command) { - stopCmd := &cobra.Command{ +func addVMStopCommand(parent *cli.Command) { + stopCmd := &cli.Command{ Use: "stop", Short: i18n.T("cmd.dev.vm.stop.short"), Long: i18n.T("cmd.dev.vm.stop.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMStop() }, } @@ -176,27 +175,27 @@ func runVMStop() error { } if !running { - fmt.Println(dimStyle.Render(i18n.T("cmd.dev.vm.not_running"))) + cli.Text(dimStyle.Render(i18n.T("cmd.dev.vm.not_running"))) return nil } - fmt.Println(i18n.T("cmd.dev.vm.stopping")) + cli.Text(i18n.T("cmd.dev.vm.stopping")) if err := d.Stop(ctx); err != nil { return err } - fmt.Println(successStyle.Render(i18n.T("common.status.stopped"))) + cli.Text(successStyle.Render(i18n.T("common.status.stopped"))) return nil } // addVMStatusCommand adds the 'devops status' command. -func addVMStatusCommand(parent *cobra.Command) { - statusCmd := &cobra.Command{ +func addVMStatusCommand(parent *cli.Command) { + statusCmd := &cli.Command{ Use: "vm-status", Short: i18n.T("cmd.dev.vm.status.short"), Long: i18n.T("cmd.dev.vm.status.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMStatus() }, } @@ -216,36 +215,36 @@ func runVMStatus() error { return err } - fmt.Println(headerStyle.Render(i18n.T("cmd.dev.vm.status_title"))) - fmt.Println() + cli.Text(headerStyle.Render(i18n.T("cmd.dev.vm.status_title"))) + cli.Line("") // Installation status if status.Installed { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), successStyle.Render(i18n.T("cmd.dev.vm.installed_yes"))) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), successStyle.Render(i18n.T("cmd.dev.vm.installed_yes"))) if status.ImageVersion != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("version")), status.ImageVersion) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("version")), status.ImageVersion) } } else { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no"))) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")})) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no"))) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")})) return nil } - fmt.Println() + cli.Line("") // Running status if status.Running { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("status")), successStyle.Render(i18n.T("common.status.running"))) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8]) - fmt.Printf("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory) - fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs) - fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime)) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), successStyle.Render(i18n.T("common.status.running"))) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8]) + cli.Print("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory) + cli.Print("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs) + cli.Print("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime)) } else { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("status")), dimStyle.Render(i18n.T("common.status.stopped"))) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), dimStyle.Render(i18n.T("common.status.stopped"))) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")})) } return nil @@ -253,27 +252,27 @@ func runVMStatus() error { func formatVMUptime(d time.Duration) string { if d < time.Minute { - return fmt.Sprintf("%ds", int(d.Seconds())) + return cli.Sprintf("%ds", int(d.Seconds())) } if d < time.Hour { - return fmt.Sprintf("%dm", int(d.Minutes())) + return cli.Sprintf("%dm", int(d.Minutes())) } if d < 24*time.Hour { - return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60) + return cli.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60) } - return fmt.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24) + return cli.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24) } // VM shell command flags var vmShellConsole bool // addVMShellCommand adds the 'devops shell' command. -func addVMShellCommand(parent *cobra.Command) { - shellCmd := &cobra.Command{ +func addVMShellCommand(parent *cli.Command) { + shellCmd := &cli.Command{ Use: "shell [-- command...]", Short: i18n.T("cmd.dev.vm.shell.short"), Long: i18n.T("cmd.dev.vm.shell.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMShell(vmShellConsole, args) }, } @@ -305,12 +304,12 @@ var ( ) // addVMServeCommand adds the 'devops serve' command. -func addVMServeCommand(parent *cobra.Command) { - serveCmd := &cobra.Command{ +func addVMServeCommand(parent *cli.Command) { + serveCmd := &cli.Command{ Use: "serve", Short: i18n.T("cmd.dev.vm.serve.short"), Long: i18n.T("cmd.dev.vm.serve.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMServe(vmServePort, vmServePath) }, } @@ -345,12 +344,12 @@ func runVMServe(port int, path string) error { var vmTestName string // addVMTestCommand adds the 'devops test' command. -func addVMTestCommand(parent *cobra.Command) { - testCmd := &cobra.Command{ +func addVMTestCommand(parent *cli.Command) { + testCmd := &cli.Command{ Use: "test [-- command...]", Short: i18n.T("cmd.dev.vm.test.short"), Long: i18n.T("cmd.dev.vm.test.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMTest(vmTestName, args) }, } @@ -388,12 +387,12 @@ var ( ) // addVMClaudeCommand adds the 'devops claude' command. -func addVMClaudeCommand(parent *cobra.Command) { - claudeCmd := &cobra.Command{ +func addVMClaudeCommand(parent *cli.Command) { + claudeCmd := &cli.Command{ Use: "claude", Short: i18n.T("cmd.dev.vm.claude.short"), Long: i18n.T("cmd.dev.vm.claude.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags) }, } @@ -430,12 +429,12 @@ func runVMClaude(noAuth bool, model string, authFlags []string) error { var vmUpdateApply bool // addVMUpdateCommand adds the 'devops update' command. -func addVMUpdateCommand(parent *cobra.Command) { - updateCmd := &cobra.Command{ +func addVMUpdateCommand(parent *cli.Command) { + updateCmd := &cli.Command{ Use: "update", Short: i18n.T("cmd.dev.vm.update.short"), Long: i18n.T("cmd.dev.vm.update.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runVMUpdate(vmUpdateApply) }, } @@ -453,58 +452,58 @@ func runVMUpdate(apply bool) error { ctx := context.Background() - fmt.Println(i18n.T("common.progress.checking_updates")) - fmt.Println() + cli.Text(i18n.T("common.progress.checking_updates")) + cli.Line("") current, latest, hasUpdate, err := d.CheckUpdate(ctx) if err != nil { - return fmt.Errorf("failed to check for updates: %w", err) + return cli.Wrap(err, "failed to check for updates") } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("current")), valueStyle.Render(current)) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest)) - fmt.Println() + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("current")), valueStyle.Render(current)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest)) + cli.Line("") if !hasUpdate { - fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date"))) + cli.Text(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date"))) return nil } - fmt.Println(warningStyle.Render(i18n.T("cmd.dev.vm.update_available"))) - fmt.Println() + cli.Text(warningStyle.Render(i18n.T("cmd.dev.vm.update_available"))) + cli.Line("") if !apply { - fmt.Println(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")})) + cli.Text(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")})) return nil } // Stop if running running, _ := d.IsRunning(ctx) if running { - fmt.Println(i18n.T("cmd.dev.vm.stopping_current")) + cli.Text(i18n.T("cmd.dev.vm.stopping_current")) _ = d.Stop(ctx) } - fmt.Println(i18n.T("cmd.dev.vm.downloading_update")) - fmt.Println() + cli.Text(i18n.T("cmd.dev.vm.downloading_update")) + cli.Line("") start := time.Now() err = d.Install(ctx, func(downloaded, total int64) { if total > 0 { pct := int(float64(downloaded) / float64(total) * 100) - fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) + cli.Print("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct) } }) - fmt.Println() + cli.Line("") if err != nil { - return fmt.Errorf("update failed: %w", err) + return cli.Wrap(err, "update failed") } elapsed := time.Since(start).Round(time.Second) - fmt.Println() - fmt.Println(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed})) + cli.Line("") + cli.Text(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed})) return nil } diff --git a/pkg/dev/cmd_work.go b/pkg/dev/cmd_work.go index 022a6f03..8ff404f2 100644 --- a/pkg/dev/cmd_work.go +++ b/pkg/dev/cmd_work.go @@ -2,7 +2,6 @@ package dev import ( "context" - "fmt" "os" "os/exec" "sort" @@ -13,7 +12,6 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" - "github.com/spf13/cobra" ) // Work command flags @@ -24,12 +22,12 @@ var ( ) // addWorkCommand adds the 'work' command to the given parent command. -func addWorkCommand(parent *cobra.Command) { - workCmd := &cobra.Command{ +func addWorkCommand(parent *cli.Command) { + workCmd := &cli.Command{ Use: "work", Short: i18n.T("cmd.dev.work.short"), Long: i18n.T("cmd.dev.work.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runWork(workRegistryPath, workStatusOnly, workAutoCommit) }, } @@ -65,7 +63,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { } if len(paths) == 0 { - fmt.Println(i18n.T("cmd.dev.no_git_repos")) + cli.Text(i18n.T("cmd.dev.no_git_repos")) return nil } @@ -75,7 +73,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { Names: names, }) if !handled { - return fmt.Errorf("git service not available") + return cli.Err("git service not available") } if err != nil { return err @@ -108,9 +106,9 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { // Auto-commit dirty repos if requested if autoCommit && len(dirtyRepos) > 0 { - fmt.Println() - fmt.Printf("%s\n", cli.TitleStyle.Render(i18n.T("cmd.dev.commit.committing"))) - fmt.Println() + cli.Line("") + cli.Print("%s\n", cli.TitleStyle.Render(i18n.T("cmd.dev.commit.committing"))) + cli.Line("") for _, s := range dirtyRepos { // PERFORM commit via agentic service @@ -119,13 +117,13 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { Name: s.Name, }) if !handled { - fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), s.Name, "agentic service not available") + cli.Print(" %s %s: %s\n", warningStyle.Render("!"), s.Name, "agentic service not available") continue } if err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) } else { - fmt.Printf(" %s %s\n", successStyle.Render("v"), s.Name) + cli.Print(" %s %s\n", successStyle.Render("v"), s.Name) } } @@ -148,32 +146,32 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { // If status only, we're done if statusOnly { if len(dirtyRepos) > 0 && !autoCommit { - fmt.Println() - fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag"))) + cli.Line("") + cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag"))) } return nil } // Push repos with unpushed commits if len(aheadRepos) == 0 { - fmt.Println() - fmt.Println(i18n.T("cmd.dev.work.all_up_to_date")) + cli.Line("") + cli.Text(i18n.T("cmd.dev.work.all_up_to_date")) return nil } - fmt.Println() - fmt.Printf("%s\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)})) + cli.Line("") + cli.Print("%s\n", i18n.T("common.count.repos_unpushed", map[string]interface{}{"Count": len(aheadRepos)})) for _, s := range aheadRepos { - fmt.Printf(" %s: %s\n", s.Name, i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead})) + cli.Print(" %s: %s\n", s.Name, i18n.T("common.count.commits", map[string]interface{}{"Count": s.Ahead})) } - fmt.Println() + cli.Line("") if !cli.Confirm(i18n.T("cmd.dev.push.confirm")) { - fmt.Println(i18n.T("cli.aborted")) + cli.Text(i18n.T("cli.aborted")) return nil } - fmt.Println() + cli.Line("") // PERFORM push for each repo var divergedRepos []git.RepoStatus @@ -184,47 +182,47 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { Name: s.Name, }) if !handled { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), s.Name, "git service not available") + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, "git service not available") continue } if err != nil { if git.IsNonFastForward(err) { - fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), s.Name, i18n.T("cmd.dev.push.diverged")) + cli.Print(" %s %s: %s\n", warningStyle.Render("!"), s.Name, i18n.T("cmd.dev.push.diverged")) divergedRepos = append(divergedRepos, s) } else { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) } } else { - fmt.Printf(" %s %s\n", successStyle.Render("v"), s.Name) + cli.Print(" %s %s\n", successStyle.Render("v"), s.Name) } } // Handle diverged repos - offer to pull and retry if len(divergedRepos) > 0 { - fmt.Println() - fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help")) + cli.Line("") + cli.Print("%s\n", i18n.T("cmd.dev.push.diverged_help")) if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) { - fmt.Println() + cli.Line("") for _, s := range divergedRepos { - fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), s.Name) + cli.Print(" %s %s...\n", dimStyle.Render("↓"), s.Name) // PERFORM pull _, _, err := bundle.Core.PERFORM(git.TaskPull{Path: s.Path, Name: s.Name}) if err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) continue } - fmt.Printf(" %s %s...\n", dimStyle.Render("↑"), s.Name) + cli.Print(" %s %s...\n", dimStyle.Render("↑"), s.Name) // PERFORM push _, _, err = bundle.Core.PERFORM(git.TaskPush{Path: s.Path, Name: s.Name}) if err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("x"), s.Name, err) continue } - fmt.Printf(" %s %s\n", successStyle.Render("v"), s.Name) + cli.Print(" %s %s\n", successStyle.Render("v"), s.Name) } } } @@ -242,7 +240,7 @@ func printStatusTable(statuses []git.RepoStatus) { } // Print header with fixed-width formatting - fmt.Printf("%-*s %8s %9s %6s %5s\n", + cli.Print("%-*s %8s %9s %6s %5s\n", nameWidth, cli.TitleStyle.Render(i18n.Label("repo")), cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_modified")), @@ -252,13 +250,13 @@ func printStatusTable(statuses []git.RepoStatus) { ) // Print separator - fmt.Println(strings.Repeat("-", nameWidth+2+10+11+8+7)) + cli.Text(strings.Repeat("-", nameWidth+2+10+11+8+7)) // Print rows for _, s := range statuses { if s.Error != nil { - paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name) - fmt.Printf("%s %s\n", + paddedName := cli.Sprintf("%-*s", nameWidth, s.Name) + cli.Print("%s %s\n", repoNameStyle.Render(paddedName), errorStyle.Render(i18n.T("cmd.dev.work.error_prefix")+" "+s.Error.Error()), ) @@ -266,28 +264,28 @@ func printStatusTable(statuses []git.RepoStatus) { } // Style numbers based on values - modStr := fmt.Sprintf("%d", s.Modified) + modStr := cli.Sprintf("%d", s.Modified) if s.Modified > 0 { modStr = dirtyStyle.Render(modStr) } else { modStr = cleanStyle.Render(modStr) } - untrackedStr := fmt.Sprintf("%d", s.Untracked) + untrackedStr := cli.Sprintf("%d", s.Untracked) if s.Untracked > 0 { untrackedStr = dirtyStyle.Render(untrackedStr) } else { untrackedStr = cleanStyle.Render(untrackedStr) } - stagedStr := fmt.Sprintf("%d", s.Staged) + stagedStr := cli.Sprintf("%d", s.Staged) if s.Staged > 0 { stagedStr = aheadStyle.Render(stagedStr) } else { stagedStr = cleanStyle.Render(stagedStr) } - aheadStr := fmt.Sprintf("%d", s.Ahead) + aheadStr := cli.Sprintf("%d", s.Ahead) if s.Ahead > 0 { aheadStr = aheadStyle.Render(aheadStr) } else { @@ -295,8 +293,8 @@ func printStatusTable(statuses []git.RepoStatus) { } // Pad name before styling to avoid ANSI code length issues - paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name) - fmt.Printf("%s %8s %9s %6s %5s\n", + paddedName := cli.Sprintf("%-*s", nameWidth, s.Name) + cli.Print("%s %8s %9s %6s %5s\n", repoNameStyle.Render(paddedName), modStr, untrackedStr, @@ -339,25 +337,25 @@ func loadRegistry(registryPath string) ([]string, map[string]string, error) { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, nil, fmt.Errorf("failed to load registry: %w", err) + return nil, nil, cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, nil, fmt.Errorf("failed to load registry: %w", err) + return nil, nil, cli.Wrap(err, "failed to load registry") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return nil, nil, fmt.Errorf("failed to scan directory: %w", err) + return nil, nil, cli.Wrap(err, "failed to scan directory") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) } } diff --git a/pkg/dev/service.go b/pkg/dev/service.go index d7786397..f39bcee9 100644 --- a/pkg/dev/service.go +++ b/pkg/dev/service.go @@ -2,12 +2,12 @@ package dev import ( "context" - "fmt" "os" "sort" "strings" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" @@ -73,7 +73,7 @@ func (s *Service) runWork(task TaskWork) error { } if len(paths) == 0 { - fmt.Println("No git repositories found") + cli.Text("No git repositories found") return nil } @@ -83,7 +83,7 @@ func (s *Service) runWork(task TaskWork) error { Names: names, }) if !handled { - return fmt.Errorf("git service not available") + return cli.Err("git service not available") } if err != nil { return err @@ -116,9 +116,9 @@ func (s *Service) runWork(task TaskWork) error { // Auto-commit dirty repos if requested if task.AutoCommit && len(dirtyRepos) > 0 { - fmt.Println() - fmt.Println("Committing changes...") - fmt.Println() + cli.Line("") + cli.Text("Committing changes...") + cli.Line("") for _, repo := range dirtyRepos { _, handled, err := s.Core().PERFORM(agentic.TaskCommit{ @@ -127,13 +127,13 @@ func (s *Service) runWork(task TaskWork) error { }) if !handled { // Agentic service not available - skip silently - fmt.Printf(" - %s: agentic service not available\n", repo.Name) + cli.Print(" - %s: agentic service not available\n", repo.Name) continue } if err != nil { - fmt.Printf(" x %s: %s\n", repo.Name, err) + cli.Print(" x %s: %s\n", repo.Name, err) } else { - fmt.Printf(" v %s\n", repo.Name) + cli.Print(" v %s\n", repo.Name) } } @@ -156,35 +156,35 @@ func (s *Service) runWork(task TaskWork) error { // If status only, we're done if task.StatusOnly { if len(dirtyRepos) > 0 && !task.AutoCommit { - fmt.Println() - fmt.Println("Use --commit flag to auto-commit dirty repos") + cli.Line("") + cli.Text("Use --commit flag to auto-commit dirty repos") } return nil } // Push repos with unpushed commits if len(aheadRepos) == 0 { - fmt.Println() - fmt.Println("All repositories are up to date") + cli.Line("") + cli.Text("All repositories are up to date") return nil } - fmt.Println() - fmt.Printf("%d repos with unpushed commits:\n", len(aheadRepos)) + cli.Line("") + cli.Print("%d repos with unpushed commits:\n", len(aheadRepos)) for _, st := range aheadRepos { - fmt.Printf(" %s: %d commits\n", st.Name, st.Ahead) + cli.Print(" %s: %d commits\n", st.Name, st.Ahead) } - fmt.Println() - fmt.Print("Push all? [y/N] ") + cli.Line("") + cli.Print("Push all? [y/N] ") var answer string - fmt.Scanln(&answer) + cli.Scanln(&answer) if strings.ToLower(answer) != "y" { - fmt.Println("Aborted") + cli.Text("Aborted") return nil } - fmt.Println() + cli.Line("") // Push each repo for _, st := range aheadRepos { @@ -193,17 +193,17 @@ func (s *Service) runWork(task TaskWork) error { Name: st.Name, }) if !handled { - fmt.Printf(" x %s: git service not available\n", st.Name) + cli.Print(" x %s: git service not available\n", st.Name) continue } if err != nil { if git.IsNonFastForward(err) { - fmt.Printf(" ! %s: branch has diverged\n", st.Name) + cli.Print(" ! %s: branch has diverged\n", st.Name) } else { - fmt.Printf(" x %s: %s\n", st.Name, err) + cli.Print(" x %s: %s\n", st.Name, err) } } else { - fmt.Printf(" v %s\n", st.Name) + cli.Print(" v %s\n", st.Name) } } @@ -217,7 +217,7 @@ func (s *Service) runStatus(task TaskStatus) error { } if len(paths) == 0 { - fmt.Println("No git repositories found") + cli.Text("No git repositories found") return nil } @@ -226,7 +226,7 @@ func (s *Service) runStatus(task TaskStatus) error { Names: names, }) if !handled { - return fmt.Errorf("git service not available") + return cli.Err("git service not available") } if err != nil { return err @@ -248,25 +248,25 @@ func (s *Service) loadRegistry(registryPath string) ([]string, map[string]string if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, nil, fmt.Errorf("failed to load registry: %w", err) + return nil, nil, cli.Wrap(err, "failed to load registry") } - fmt.Printf("Registry: %s\n\n", registryPath) + cli.Print("Registry: %s\n\n", registryPath) } else { registryPath, err = repos.FindRegistry() if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, nil, fmt.Errorf("failed to load registry: %w", err) + return nil, nil, cli.Wrap(err, "failed to load registry") } - fmt.Printf("Registry: %s\n\n", registryPath) + cli.Print("Registry: %s\n\n", registryPath) } else { // Fallback: scan current directory cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return nil, nil, fmt.Errorf("failed to scan directory: %w", err) + return nil, nil, cli.Wrap(err, "failed to scan directory") } - fmt.Printf("Scanning: %s\n\n", cwd) + cli.Print("Scanning: %s\n\n", cwd) } } @@ -293,20 +293,20 @@ func (s *Service) printStatusTable(statuses []git.RepoStatus) { } // Print header - fmt.Printf("%-*s %8s %9s %6s %5s\n", + cli.Print("%-*s %8s %9s %6s %5s\n", nameWidth, "Repo", "Modified", "Untracked", "Staged", "Ahead") // Print separator - fmt.Println(strings.Repeat("-", nameWidth+2+10+11+8+7)) + cli.Text(strings.Repeat("-", nameWidth+2+10+11+8+7)) // Print rows for _, st := range statuses { if st.Error != nil { - fmt.Printf("%-*s error: %s\n", nameWidth, st.Name, st.Error) + cli.Print("%-*s error: %s\n", nameWidth, st.Name, st.Error) continue } - fmt.Printf("%-*s %8d %9d %6d %5d\n", + cli.Print("%-*s %8d %9d %6d %5d\n", nameWidth, st.Name, st.Modified, st.Untracked, st.Staged, st.Ahead) } diff --git a/pkg/docs/cmd_commands.go b/pkg/docs/cmd_commands.go index fa1e4195..e17dabb9 100644 --- a/pkg/docs/cmd_commands.go +++ b/pkg/docs/cmd_commands.go @@ -8,16 +8,13 @@ // to a central location for unified documentation builds. package docs -import ( - "github.com/host-uk/core/pkg/cli" - "github.com/spf13/cobra" -) +import "github.com/host-uk/core/pkg/cli" func init() { cli.RegisterCommands(AddDocsCommands) } // AddDocsCommands registers the 'docs' command and all subcommands. -func AddDocsCommands(root *cobra.Command) { +func AddDocsCommands(root *cli.Command) { root.AddCommand(docsCmd) } diff --git a/pkg/docs/cmd_docs.go b/pkg/docs/cmd_docs.go index 2dc58e22..0d4e20c3 100644 --- a/pkg/docs/cmd_docs.go +++ b/pkg/docs/cmd_docs.go @@ -4,7 +4,6 @@ package docs import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // Style and utility aliases from shared @@ -20,7 +19,7 @@ var ( docsFileStyle = cli.InfoStyle ) -var docsCmd = &cobra.Command{ +var docsCmd = &cli.Command{ Use: "docs", Short: i18n.T("cmd.docs.short"), Long: i18n.T("cmd.docs.long"), diff --git a/pkg/docs/cmd_list.go b/pkg/docs/cmd_list.go index a3323647..994a3875 100644 --- a/pkg/docs/cmd_list.go +++ b/pkg/docs/cmd_list.go @@ -1,22 +1,20 @@ package docs import ( - "fmt" "strings" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // Flag variable for list command var docsListRegistryPath string -var docsListCmd = &cobra.Command{ +var docsListCmd = &cli.Command{ Use: "list", Short: i18n.T("cmd.docs.list.short"), Long: i18n.T("cmd.docs.list.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runDocsList(docsListRegistryPath) }, } @@ -31,14 +29,14 @@ func runDocsList(registryPath string) error { return err } - fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n", + cli.Print("\n%-20s %-8s %-8s %-10s %s\n", headerStyle.Render(i18n.Label("repo")), headerStyle.Render(i18n.T("cmd.docs.list.header.readme")), headerStyle.Render(i18n.T("cmd.docs.list.header.claude")), headerStyle.Render(i18n.T("cmd.docs.list.header.changelog")), headerStyle.Render(i18n.T("cmd.docs.list.header.docs")), ) - fmt.Println(strings.Repeat("─", 70)) + cli.Text(strings.Repeat("─", 70)) var withDocs, withoutDocs int for _, repo := range reg.List() { @@ -53,7 +51,7 @@ func runDocsList(registryPath string) error { docsDir = docsFoundStyle.Render(i18n.T("common.count.files", map[string]interface{}{"Count": len(info.DocsFiles)})) } - fmt.Printf("%-20s %-8s %-8s %-10s %s\n", + cli.Print("%-20s %-8s %-8s %-10s %s\n", repoNameStyle.Render(info.Name), readme, claude, @@ -68,8 +66,8 @@ func runDocsList(registryPath string) error { } } - fmt.Println() - fmt.Printf("%s %s\n", + cli.Line("") + cli.Print("%s %s\n", cli.Label(i18n.Label("coverage")), i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}), ) diff --git a/pkg/docs/cmd_scan.go b/pkg/docs/cmd_scan.go index 3b3b2432..b6fd6107 100644 --- a/pkg/docs/cmd_scan.go +++ b/pkg/docs/cmd_scan.go @@ -1,12 +1,12 @@ package docs import ( - "fmt" "io/fs" "os" "path/filepath" "strings" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" ) @@ -30,7 +30,7 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) { if registryPath != "" { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, "", fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err) + return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry")) } basePath = filepath.Dir(registryPath) } else { @@ -38,14 +38,14 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) { if err == nil { reg, err = repos.LoadRegistry(registryPath) if err != nil { - return nil, "", fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err) + return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry")) } basePath = filepath.Dir(registryPath) } else { cwd, _ := os.Getwd() reg, err = repos.ScanDirectory(cwd) if err != nil { - return nil, "", fmt.Errorf("%s: %w", i18n.T("i18n.fail.scan", "directory"), err) + return nil, "", cli.Wrap(err, i18n.T("i18n.fail.scan", "directory")) } basePath = cwd } diff --git a/pkg/docs/cmd_sync.go b/pkg/docs/cmd_sync.go index bd8a54d2..2c33ba49 100644 --- a/pkg/docs/cmd_sync.go +++ b/pkg/docs/cmd_sync.go @@ -1,13 +1,12 @@ package docs import ( - "fmt" "os" "path/filepath" "strings" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // Flag variables for sync command @@ -17,11 +16,11 @@ var ( docsSyncOutputDir string ) -var docsSyncCmd = &cobra.Command{ +var docsSyncCmd = &cli.Command{ Use: "sync", Short: i18n.T("cmd.docs.sync.short"), Long: i18n.T("cmd.docs.sync.long"), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun) }, } @@ -83,45 +82,45 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { } if len(docsInfo) == 0 { - fmt.Println(i18n.T("cmd.docs.sync.no_docs_found")) + cli.Text(i18n.T("cmd.docs.sync.no_docs_found")) return nil } - fmt.Printf("\n%s %s\n\n", dimStyle.Render(i18n.T("cmd.docs.sync.found_label")), i18n.T("cmd.docs.sync.repos_with_docs", map[string]interface{}{"Count": len(docsInfo)})) + cli.Print("\n%s %s\n\n", dimStyle.Render(i18n.T("cmd.docs.sync.found_label")), i18n.T("cmd.docs.sync.repos_with_docs", map[string]interface{}{"Count": len(docsInfo)})) // Show what will be synced var totalFiles int for _, info := range docsInfo { totalFiles += len(info.DocsFiles) outName := packageOutputName(info.Name) - fmt.Printf(" %s → %s %s\n", + cli.Print(" %s → %s %s\n", repoNameStyle.Render(info.Name), docsFileStyle.Render("packages/"+outName+"/"), dimStyle.Render(i18n.T("cmd.docs.sync.files_count", map[string]interface{}{"Count": len(info.DocsFiles)}))) for _, f := range info.DocsFiles { - fmt.Printf(" %s\n", dimStyle.Render(f)) + cli.Print(" %s\n", dimStyle.Render(f)) } } - fmt.Printf("\n%s %s\n", + cli.Print("\n%s %s\n", dimStyle.Render(i18n.Label("total")), i18n.T("cmd.docs.sync.total_summary", map[string]interface{}{"Files": totalFiles, "Repos": len(docsInfo), "Output": outputDir})) if dryRun { - fmt.Printf("\n%s\n", dimStyle.Render(i18n.T("cmd.docs.sync.dry_run_notice"))) + cli.Print("\n%s\n", dimStyle.Render(i18n.T("cmd.docs.sync.dry_run_notice"))) return nil } // Confirm - fmt.Println() + cli.Line("") if !confirm(i18n.T("cmd.docs.sync.confirm")) { - fmt.Println(i18n.T("common.prompt.abort")) + cli.Text(i18n.T("common.prompt.abort")) return nil } // Sync docs - fmt.Println() + cli.Line("") var synced int for _, info := range docsInfo { outName := packageOutputName(info.Name) @@ -131,7 +130,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { os.RemoveAll(repoOutDir) if err := os.MkdirAll(repoOutDir, 0755); err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err) continue } @@ -142,15 +141,15 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { dst := filepath.Join(repoOutDir, f) os.MkdirAll(filepath.Dir(dst), 0755) if err := copyFile(src, dst); err != nil { - fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), f, err) + cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err) } } - fmt.Printf(" %s %s → packages/%s/\n", successStyle.Render("✓"), info.Name, outName) + cli.Print(" %s %s → packages/%s/\n", successStyle.Render("✓"), info.Name, outName) synced++ } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.sync")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.sync")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced})) return nil } diff --git a/pkg/go/cmd_format.go b/pkg/go/cmd_format.go index 29c86674..59ce1c3b 100644 --- a/pkg/go/cmd_format.go +++ b/pkg/go/cmd_format.go @@ -4,8 +4,8 @@ import ( "os" "os/exec" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) var ( @@ -14,12 +14,12 @@ var ( fmtCheck bool ) -func addGoFmtCommand(parent *cobra.Command) { - fmtCmd := &cobra.Command{ +func addGoFmtCommand(parent *cli.Command) { + fmtCmd := &cli.Command{ Use: "fmt", Short: "Format Go code", Long: "Format Go code using goimports or gofmt", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { fmtArgs := []string{} if fmtFix { fmtArgs = append(fmtArgs, "-w") @@ -55,12 +55,12 @@ func addGoFmtCommand(parent *cobra.Command) { var lintFix bool -func addGoLintCommand(parent *cobra.Command) { - lintCmd := &cobra.Command{ +func addGoLintCommand(parent *cli.Command) { + lintCmd := &cli.Command{ Use: "lint", Short: "Run golangci-lint", Long: "Run golangci-lint for comprehensive static analysis", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { lintArgs := []string{"run"} if lintFix { lintArgs = append(lintArgs, "--fix") diff --git a/pkg/go/cmd_go.go b/pkg/go/cmd_go.go index e4b68933..7aebd9f0 100644 --- a/pkg/go/cmd_go.go +++ b/pkg/go/cmd_go.go @@ -6,7 +6,6 @@ package gocmd import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) // Style aliases for shared styles @@ -17,8 +16,8 @@ var ( ) // AddGoCommands adds Go development commands. -func AddGoCommands(root *cobra.Command) { - goCmd := &cobra.Command{ +func AddGoCommands(root *cli.Command) { + goCmd := &cli.Command{ Use: "go", Short: i18n.T("cmd.go.short"), Long: i18n.T("cmd.go.long"), diff --git a/pkg/go/cmd_gotest.go b/pkg/go/cmd_gotest.go index c77719c4..487cbd34 100644 --- a/pkg/go/cmd_gotest.go +++ b/pkg/go/cmd_gotest.go @@ -11,7 +11,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) var ( @@ -24,12 +23,12 @@ var ( testVerbose bool ) -func addGoTestCommand(parent *cobra.Command) { - testCmd := &cobra.Command{ +func addGoTestCommand(parent *cli.Command) { + testCmd := &cli.Command{ Use: "test", Short: "Run Go tests", Long: "Run Go tests with optional coverage, filtering, and race detection", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose) }, } @@ -74,9 +73,9 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo args = append(args, pkg) if !jsonOut { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("package")), pkg) - fmt.Println() + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), pkg) + cli.Line("") } cmd := exec.Command("go", args...) @@ -101,34 +100,34 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo cov := parseOverallCoverage(outputStr) if jsonOut { - fmt.Printf(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`, + cli.Print(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`, passed, failed, skipped, cov, cmd.ProcessState.ExitCode()) - fmt.Println() + cli.Line("") return err } // Print filtered output if verbose or failed if verbose || err != nil { - fmt.Println(outputStr) + cli.Text(outputStr) } // Summary if err == nil { - fmt.Printf(" %s %s\n", successStyle.Render(cli.SymbolCheck), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass")) + cli.Print(" %s %s\n", successStyle.Render(cli.SymbolCheck), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass")) } else { - fmt.Printf(" %s %s, %s\n", errorStyle.Render(cli.SymbolCross), + cli.Print(" %s %s, %s\n", errorStyle.Render(cli.SymbolCross), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass"), i18n.T("i18n.count.test", failed)+" "+i18n.T("i18n.done.fail")) } if cov > 0 { - fmt.Printf("\n %s %s\n", cli.ProgressLabel(i18n.Label("coverage")), cli.FormatCoverage(cov)) + cli.Print("\n %s %s\n", cli.ProgressLabel(i18n.Label("coverage")), cli.FormatCoverage(cov)) } if err == nil { - fmt.Printf("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) + cli.Print("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) } else { - fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("i18n.done.fail"))) + cli.Print("\n%s\n", errorStyle.Render(i18n.T("i18n.done.fail"))) } return err @@ -168,18 +167,18 @@ var ( covThreshold float64 ) -func addGoCovCommand(parent *cobra.Command) { - covCmd := &cobra.Command{ +func addGoCovCommand(parent *cli.Command) { + covCmd := &cli.Command{ Use: "cov", Short: "Run tests with coverage report", Long: "Run tests with detailed coverage reports, HTML output, and threshold checking", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { pkg := covPkg if pkg == "" { // Auto-discover packages with tests pkgs, err := findTestPackages(".") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.find", "test packages"), err) + return cli.Wrap(err, i18n.T("i18n.fail.find", "test packages")) } if len(pkgs) == 0 { return errors.New("no test packages found") @@ -190,20 +189,20 @@ func addGoCovCommand(parent *cobra.Command) { // Create temp file for coverage data covFile, err := os.CreateTemp("", "coverage-*.out") if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.create", "coverage file"), err) + return cli.Wrap(err, i18n.T("i18n.fail.create", "coverage file")) } covPath := covFile.Name() covFile.Close() defer os.Remove(covPath) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("coverage")), i18n.ProgressSubject("run", "tests")) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("coverage")), i18n.ProgressSubject("run", "tests")) // Truncate package list if too long for display displayPkg := pkg if len(displayPkg) > 60 { displayPkg = displayPkg[:57] + "..." } - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("package")), displayPkg) - fmt.Println() + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), displayPkg) + cli.Line("") // Run tests with coverage // We need to split pkg into individual arguments if it contains spaces @@ -224,7 +223,7 @@ func addGoCovCommand(parent *cobra.Command) { if testErr != nil { return testErr } - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "coverage"), err) + return cli.Wrap(err, i18n.T("i18n.fail.get", "coverage")) } // Parse total coverage from last line @@ -243,17 +242,17 @@ func addGoCovCommand(parent *cobra.Command) { } // Print coverage summary - fmt.Println() - fmt.Printf(" %s %s\n", cli.ProgressLabel(i18n.Label("total")), cli.FormatCoverage(totalCov)) + cli.Line("") + cli.Print(" %s %s\n", cli.ProgressLabel(i18n.Label("total")), cli.FormatCoverage(totalCov)) // Generate HTML if requested if covHTML || covOpen { htmlPath := "coverage.html" htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) if err := htmlCmd.Run(); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.generate", "HTML"), err) + return cli.Wrap(err, i18n.T("i18n.fail.generate", "HTML")) } - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("html")), htmlPath) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("html")), htmlPath) if covOpen { // Open in browser @@ -264,7 +263,7 @@ func addGoCovCommand(parent *cobra.Command) { case exec.Command("which", "xdg-open").Run() == nil: openCmd = exec.Command("xdg-open", htmlPath) default: - fmt.Printf(" %s\n", dimStyle.Render("Open coverage.html in your browser")) + cli.Print(" %s\n", dimStyle.Render("Open coverage.html in your browser")) } if openCmd != nil { openCmd.Run() @@ -274,7 +273,7 @@ func addGoCovCommand(parent *cobra.Command) { // Check threshold if covThreshold > 0 && totalCov < covThreshold { - fmt.Printf("\n%s %.1f%% < %.1f%%\n", errorStyle.Render(i18n.T("i18n.fail.meet", "threshold")), totalCov, covThreshold) + cli.Print("\n%s %.1f%% < %.1f%%\n", errorStyle.Render(i18n.T("i18n.fail.meet", "threshold")), totalCov, covThreshold) return errors.New("coverage below threshold") } @@ -282,7 +281,7 @@ func addGoCovCommand(parent *cobra.Command) { return testErr } - fmt.Printf("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) + cli.Print("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) return nil }, } diff --git a/pkg/go/cmd_qa.go b/pkg/go/cmd_qa.go index 3cdd5abb..310c4c6b 100644 --- a/pkg/go/cmd_qa.go +++ b/pkg/go/cmd_qa.go @@ -2,20 +2,18 @@ package gocmd import ( "context" - "fmt" "os" "os/exec" "time" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) var qaFix bool -func addGoQACommand(parent *cobra.Command) { - qaCmd := &cobra.Command{ +func addGoQACommand(parent *cli.Command) { + qaCmd := &cli.Command{ Use: "qa", Short: "Run QA checks", Long: "Run code quality checks: formatting, vetting, linting, and testing", @@ -25,65 +23,67 @@ func addGoQACommand(parent *cobra.Command) { qaCmd.PersistentFlags().BoolVar(&qaFix, "fix", false, i18n.T("common.flag.fix")) // Subcommands for individual checks - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "fmt", Short: "Check/fix code formatting", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"fmt"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "vet", Short: "Run go vet", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"vet"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"vet"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "lint", Short: "Run golangci-lint", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"lint"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"lint"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "test", Short: "Run tests", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"test"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"test"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "race", Short: "Run tests with race detector", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"race"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"race"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "vuln", Short: "Check for vulnerabilities", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"vuln"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"vuln"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "sec", Short: "Run security scanner", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"sec"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"sec"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "quick", Short: "Quick QA: fmt, vet, lint", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint"}) }, + RunE: func(cmd *cli.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint"}) }, }) - qaCmd.AddCommand(&cobra.Command{ + qaCmd.AddCommand(&cli.Command{ Use: "full", Short: "Full QA: all checks including race, vuln, sec", - RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint", "test", "race", "vuln", "sec"}) }, + RunE: func(cmd *cli.Command, args []string) error { + return runQAChecks([]string{"fmt", "vet", "lint", "test", "race", "vuln", "sec"}) + }, }) parent.AddCommand(qaCmd) } // runGoQADefault runs the default QA checks (fmt, vet, lint, test) -func runGoQADefault(cmd *cobra.Command, args []string) error { +func runGoQADefault(cmd *cli.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint", "test"}) } @@ -97,15 +97,15 @@ type QACheck struct { func runQAChecks(checkNames []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Wrap(err, i18n.T("i18n.fail.get", "working directory")) } // Detect if this is a Go project if _, err := os.Stat("go.mod"); os.IsNotExist(err) { - return fmt.Errorf("not a Go project (no %s found)", i18n.T("gram.word.go_mod")) + return cli.Err("not a Go project (no %s found)", i18n.T("gram.word.go_mod")) } - fmt.Printf("%s %s\n\n", cli.DimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "Go QA")) + cli.Print("%s %s\n\n", cli.DimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "Go QA")) checks := buildChecksForNames(checkNames) @@ -115,23 +115,23 @@ func runQAChecks(checkNames []string) error { failed := 0 for _, check := range checks { - fmt.Printf("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name)) + cli.Print("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name)) if err := runCheck(ctx, cwd, check); err != nil { - fmt.Printf(" %s %s\n", cli.ErrorStyle.Render(cli.SymbolCross), err.Error()) + cli.Print(" %s %s\n", cli.ErrorStyle.Render(cli.SymbolCross), err.Error()) failed++ } else { - fmt.Printf(" %s %s\n", cli.SuccessStyle.Render(cli.SymbolCheck), i18n.T("i18n.done.pass")) + cli.Print(" %s %s\n", cli.SuccessStyle.Render(cli.SymbolCheck), i18n.T("i18n.done.pass")) passed++ } } // Summary - fmt.Println() + cli.Line("") duration := time.Since(startTime).Round(time.Millisecond) if failed > 0 { - fmt.Printf("%s %s, %s (%s)\n", + cli.Print("%s %s, %s (%s)\n", cli.ErrorStyle.Render(cli.SymbolCross), i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), i18n.T("i18n.count.check", failed)+" "+i18n.T("i18n.done.fail"), @@ -139,7 +139,7 @@ func runQAChecks(checkNames []string) error { os.Exit(1) } - fmt.Printf("%s %s (%s)\n", + cli.Print("%s %s (%s)\n", cli.SuccessStyle.Render(cli.SymbolCheck), i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), duration) @@ -214,7 +214,7 @@ func lintArgs(fix bool) []string { func runCheck(ctx context.Context, dir string, check QACheck) error { // Check if command exists if _, err := exec.LookPath(check.Command); err != nil { - return fmt.Errorf("%s: %s", check.Command, i18n.T("i18n.done.miss")) + return cli.Err("%s: %s", check.Command, i18n.T("i18n.done.miss")) } cmd := exec.CommandContext(ctx, check.Command, check.Args...) @@ -228,8 +228,8 @@ func runCheck(ctx context.Context, dir string, check QACheck) error { } if len(output) > 0 { // Show files that need formatting - fmt.Print(string(output)) - return fmt.Errorf("%s (use --fix)", i18n.T("i18n.fail.format", i18n.T("i18n.count.file", len(output)))) + cli.Print(string(output)) + return cli.Err("%s (use --fix)", i18n.T("i18n.fail.format", i18n.T("i18n.count.file", len(output)))) } return nil } diff --git a/pkg/go/cmd_tools.go b/pkg/go/cmd_tools.go index f192ebe1..fd080ff9 100644 --- a/pkg/go/cmd_tools.go +++ b/pkg/go/cmd_tools.go @@ -2,13 +2,12 @@ package gocmd import ( "errors" - "fmt" "os" "os/exec" "path/filepath" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/spf13/cobra" ) var ( @@ -16,12 +15,12 @@ var ( installNoCgo bool ) -func addGoInstallCommand(parent *cobra.Command) { - installCmd := &cobra.Command{ +func addGoInstallCommand(parent *cli.Command) { + installCmd := &cli.Command{ Use: "install [path]", Short: "Install Go binary", Long: "Install Go binary to $GOPATH/bin", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { // Get install path from args or default to current dir installPath := "./..." if len(args) > 0 { @@ -39,10 +38,10 @@ func addGoInstallCommand(parent *cobra.Command) { } } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.Progress("install")) - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("path")), installPath) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.Progress("install")) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("path")), installPath) if installNoCgo { - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("cgo")), "disabled") + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("cgo")), "disabled") } cmdArgs := []string{"install"} @@ -59,7 +58,7 @@ func addGoInstallCommand(parent *cobra.Command) { execCmd.Stderr = os.Stderr if err := execCmd.Run(); err != nil { - fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("i18n.fail.install", "binary"))) + cli.Print("\n%s\n", errorStyle.Render(i18n.T("i18n.fail.install", "binary"))) return err } @@ -71,7 +70,7 @@ func addGoInstallCommand(parent *cobra.Command) { } binDir := filepath.Join(gopath, "bin") - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.install")), binDir) + cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.install")), binDir) return nil }, } @@ -82,18 +81,18 @@ func addGoInstallCommand(parent *cobra.Command) { parent.AddCommand(installCmd) } -func addGoModCommand(parent *cobra.Command) { - modCmd := &cobra.Command{ +func addGoModCommand(parent *cli.Command) { + modCmd := &cli.Command{ Use: "mod", Short: "Module management", Long: "Go module management commands", } // tidy - tidyCmd := &cobra.Command{ + tidyCmd := &cli.Command{ Use: "tidy", Short: "Run go mod tidy", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "mod", "tidy") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -102,10 +101,10 @@ func addGoModCommand(parent *cobra.Command) { } // download - downloadCmd := &cobra.Command{ + downloadCmd := &cli.Command{ Use: "download", Short: "Download module dependencies", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "mod", "download") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -114,10 +113,10 @@ func addGoModCommand(parent *cobra.Command) { } // verify - verifyCmd := &cobra.Command{ + verifyCmd := &cli.Command{ Use: "verify", Short: "Verify module checksums", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "mod", "verify") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -126,10 +125,10 @@ func addGoModCommand(parent *cobra.Command) { } // graph - graphCmd := &cobra.Command{ + graphCmd := &cli.Command{ Use: "graph", Short: "Print module dependency graph", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "mod", "graph") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -144,18 +143,18 @@ func addGoModCommand(parent *cobra.Command) { parent.AddCommand(modCmd) } -func addGoWorkCommand(parent *cobra.Command) { - workCmd := &cobra.Command{ +func addGoWorkCommand(parent *cli.Command) { + workCmd := &cli.Command{ Use: "work", Short: "Workspace management", Long: "Go workspace management commands", } // sync - syncCmd := &cobra.Command{ + syncCmd := &cli.Command{ Use: "sync", Short: "Sync workspace modules", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "work", "sync") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -164,10 +163,10 @@ func addGoWorkCommand(parent *cobra.Command) { } // init - initCmd := &cobra.Command{ + initCmd := &cli.Command{ Use: "init", Short: "Initialise a new workspace", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { execCmd := exec.Command("go", "work", "init") execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr @@ -186,10 +185,10 @@ func addGoWorkCommand(parent *cobra.Command) { } // use - useCmd := &cobra.Command{ + useCmd := &cli.Command{ Use: "use [modules...]", Short: "Add modules to workspace", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cli.Command, args []string) error { if len(args) == 0 { // Auto-detect modules modules := findGoModules(".") @@ -203,7 +202,7 @@ func addGoWorkCommand(parent *cobra.Command) { if err := execCmd.Run(); err != nil { return err } - fmt.Printf("%s %s\n", successStyle.Render(i18n.T("i18n.done.add")), mod) + cli.Print("%s %s\n", successStyle.Render(i18n.T("i18n.done.add")), mod) } return nil } diff --git a/pkg/php/cmd_build.go b/pkg/php/cmd_build.go index 8fd23f48..a6cd45d0 100644 --- a/pkg/php/cmd_build.go +++ b/pkg/php/cmd_build.go @@ -3,10 +3,10 @@ package php import ( "context" "errors" - "fmt" "os" "strings" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" ) @@ -31,7 +31,7 @@ func addPHPBuildCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } ctx := context.Background() @@ -87,22 +87,22 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO return errors.New(i18n.T("cmd.php.error.not_php")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker")) // Show detected configuration config, err := DetectDockerfileConfig(projectDir) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.detect", "project configuration"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.detect", "project configuration"), err) } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.php_version")), config.PHPVersion) - fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.laravel")), config.IsLaravel) - fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.octane")), config.HasOctane) - fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.frontend")), config.HasAssets) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.php_version")), config.PHPVersion) + cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.laravel")), config.IsLaravel) + cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.octane")), config.HasOctane) + cli.Print("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.frontend")), config.HasAssets) if len(config.PHPExtensions) > 0 { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.extensions")), strings.Join(config.PHPExtensions, ", ")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.extensions")), strings.Join(config.PHPExtensions, ", ")) } - fmt.Println() + cli.Line("") // Build options buildOpts := DockerBuildOptions{ @@ -128,18 +128,18 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO buildOpts.Tag = "latest" } - fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), buildOpts.ImageName, buildOpts.Tag) + cli.Print("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), buildOpts.ImageName, buildOpts.Tag) if opts.Platform != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.platform")), opts.Platform) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.platform")), opts.Platform) } - fmt.Println() + cli.Line("") if err := BuildDocker(ctx, buildOpts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.build"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.build"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Docker image built"})) - fmt.Printf("%s docker run -p 80:80 -p 443:443 %s:%s\n", + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Docker image built"})) + cli.Print("%s docker run -p 80:80 -p 443:443 %s:%s\n", dimStyle.Render(i18n.T("cmd.php.build.docker_run_with")), buildOpts.ImageName, buildOpts.Tag) @@ -151,7 +151,7 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu return errors.New(i18n.T("cmd.php.error.not_php")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit")) buildOpts := LinuxKitBuildOptions{ ProjectDir: projectDir, @@ -168,15 +168,15 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu buildOpts.Template = "server-php" } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("template")), buildOpts.Template) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format) - fmt.Println() + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("template")), buildOpts.Template) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format) + cli.Line("") if err := BuildLinuxKit(ctx, buildOpts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.build"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.build"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "LinuxKit image built"})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "LinuxKit image built"})) return nil } @@ -224,8 +224,8 @@ func addPHPServeCommand(parent *cobra.Command) { Output: os.Stdout, } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "production container")) - fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), imageName, func() string { + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "production container")) + cli.Print("%s %s:%s\n", dimStyle.Render(i18n.Label("image")), imageName, func() string { if serveTag == "" { return "latest" } @@ -241,16 +241,16 @@ func addPHPServeCommand(parent *cobra.Command) { effectiveHTTPSPort = 443 } - fmt.Printf("%s http://localhost:%d, https://localhost:%d\n", + cli.Print("%s http://localhost:%d, https://localhost:%d\n", dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) - fmt.Println() + cli.Line("") if err := ServeProduction(ctx, opts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.start", "container"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.start", "container"), err) } if !serveDetach { - fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.stopped")) + cli.Print("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.stopped")) } return nil @@ -277,10 +277,10 @@ func addPHPShellCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]})) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]})) if err := Shell(ctx, args[0]); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.open", "shell"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.open", "shell"), err) } return nil diff --git a/pkg/php/cmd_deploy.go b/pkg/php/cmd_deploy.go index 07ebbc80..259002e5 100644 --- a/pkg/php/cmd_deploy.go +++ b/pkg/php/cmd_deploy.go @@ -2,7 +2,6 @@ package php import ( "context" - "fmt" "os" "time" @@ -46,7 +45,7 @@ func addPHPDeployCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } env := EnvProduction @@ -54,7 +53,7 @@ func addPHPDeployCommand(parent *cobra.Command) { env = EnvStaging } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env})) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env})) ctx := context.Background() @@ -67,19 +66,19 @@ func addPHPDeployCommand(parent *cobra.Command) { status, err := Deploy(ctx, opts) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err) } printDeploymentStatus(status) if deployWait { if IsDeploymentSuccessful(status.Status) { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"})) } else { - fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status})) + cli.Print("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status})) } } else { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy.triggered")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy.triggered")) } return nil @@ -106,7 +105,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } env := EnvProduction @@ -114,7 +113,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) { env = EnvStaging } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.ProgressSubject("check", "deployment status")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.ProgressSubject("check", "deployment status")) ctx := context.Background() @@ -126,7 +125,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) { status, err := DeployStatus(ctx, opts) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "status"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "status"), err) } printDeploymentStatus(status) @@ -155,7 +154,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } env := EnvProduction @@ -163,7 +162,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) { env = EnvStaging } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env})) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env})) ctx := context.Background() @@ -176,19 +175,19 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) { status, err := Rollback(ctx, opts) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err) } printDeploymentStatus(status) if rollbackWait { if IsDeploymentSuccessful(status.Status) { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"})) } else { - fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status})) + cli.Print("\n%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status})) } } else { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy_rollback.triggered")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.deploy_rollback.triggered")) } return nil @@ -215,7 +214,7 @@ func addPHPDeployListCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } env := EnvProduction @@ -228,17 +227,17 @@ func addPHPDeployListCommand(parent *cobra.Command) { limit = 10 } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_list.recent", map[string]interface{}{"Environment": env})) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_list.recent", map[string]interface{}{"Environment": env})) ctx := context.Background() deployments, err := ListDeployments(ctx, cwd, env, limit) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.list", "deployments"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.list", "deployments"), err) } if len(deployments) == 0 { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.deploy_list.none_found")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.deploy_list.none_found")) return nil } @@ -266,18 +265,18 @@ func printDeploymentStatus(status *DeploymentStatus) { statusStyle = phpDeployFailedStyle } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("status")), statusStyle.Render(status.Status)) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("status")), statusStyle.Render(status.Status)) if status.ID != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.id")), status.ID) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.id")), status.ID) } if status.URL != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("url")), linkStyle.Render(status.URL)) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("url")), linkStyle.Render(status.URL)) } if status.Branch != "" { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.branch")), status.Branch) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.branch")), status.Branch) } if status.Commit != "" { @@ -285,26 +284,26 @@ func printDeploymentStatus(status *DeploymentStatus) { if len(commit) > 7 { commit = commit[:7] } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.commit")), commit) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.commit")), commit) if status.CommitMessage != "" { // Truncate long messages msg := status.CommitMessage if len(msg) > 60 { msg = msg[:57] + "..." } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.message")), msg) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.message")), msg) } } if !status.StartedAt.IsZero() { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("started")), status.StartedAt.Format(time.RFC3339)) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("started")), status.StartedAt.Format(time.RFC3339)) } if !status.CompletedAt.IsZero() { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.completed")), status.CompletedAt.Format(time.RFC3339)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.completed")), status.CompletedAt.Format(time.RFC3339)) if !status.StartedAt.IsZero() { duration := status.CompletedAt.Sub(status.StartedAt) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.duration")), duration.Round(time.Second)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.duration")), duration.Round(time.Second)) } } } @@ -340,24 +339,23 @@ func printDeploymentSummary(index int, status *DeploymentStatus) { age = i18n.TimeAgo(status.StartedAt) } - fmt.Printf(" %s %s %s", - dimStyle.Render(fmt.Sprintf("#%d", index)), - statusStyle.Render(fmt.Sprintf("[%s]", status.Status)), + cli.Print(" %s %s %s", + dimStyle.Render(cli.Sprintf("#%d", index)), + statusStyle.Render(cli.Sprintf("[%s]", status.Status)), id, ) if commit != "" { - fmt.Printf(" %s", commit) + cli.Print(" %s", commit) } if msg != "" { - fmt.Printf(" - %s", msg) + cli.Print(" - %s", msg) } if age != "" { - fmt.Printf(" %s", dimStyle.Render(fmt.Sprintf("(%s)", age))) + cli.Print(" %s", dimStyle.Render(cli.Sprintf("(%s)", age))) } - fmt.Println() + cli.Line("") } - diff --git a/pkg/php/cmd_dev.go b/pkg/php/cmd_dev.go index aa8dc36c..3f3de563 100644 --- a/pkg/php/cmd_dev.go +++ b/pkg/php/cmd_dev.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "errors" - "fmt" "os" "os/signal" "strings" @@ -12,6 +11,7 @@ import ( "time" "github.com/charmbracelet/lipgloss" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" ) @@ -68,7 +68,7 @@ type phpDevOptions struct { func runPHPDev(opts phpDevOptions) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.Err("failed to get working directory: %w", err) } // Check if this is a Laravel project @@ -82,15 +82,15 @@ func runPHPDev(opts phpDevOptions) error { appName = "Laravel" } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName})) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName})) // Detect services services := DetectServices(cwd) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services")) for _, svc := range services { - fmt.Printf(" %s %s\n", successStyle.Render("*"), svc) + cli.Print(" %s %s\n", successStyle.Render("*"), svc) } - fmt.Println() + cli.Line("") // Setup options port := opts.Port @@ -121,41 +121,41 @@ func runPHPDev(opts phpDevOptions) error { go func() { <-sigCh - fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.shutting_down")) + cli.Print("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.shutting_down")) cancel() }() if err := server.Start(ctx, devOpts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.start", "services"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.start", "services"), err) } // Print status - fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.running")), i18n.T("cmd.php.dev.services_started")) + cli.Print("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.running")), i18n.T("cmd.php.dev.services_started")) printServiceStatuses(server.Status()) - fmt.Println() + cli.Line("") // Print URLs appURL := GetLaravelAppURL(cwd) if appURL == "" { if opts.HTTPS { - appURL = fmt.Sprintf("https://localhost:%d", port) + appURL = cli.Sprintf("https://localhost:%d", port) } else { - appURL = fmt.Sprintf("http://localhost:%d", port) + appURL = cli.Sprintf("http://localhost:%d", port) } } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL)) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL)) // Check for Vite if !opts.NoVite && containsService(services, ServiceVite) { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173")) } - fmt.Printf("\n%s\n\n", dimStyle.Render(i18n.T("cmd.php.dev.press_ctrl_c"))) + cli.Print("\n%s\n\n", dimStyle.Render(i18n.T("cmd.php.dev.press_ctrl_c"))) // Stream unified logs logsReader, err := server.Logs("", true) if err != nil { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("i18n.fail.get", "logs")) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("i18n.fail.get", "logs")) } else { defer logsReader.Close() @@ -174,10 +174,10 @@ func runPHPDev(opts phpDevOptions) error { shutdown: // Stop services if err := server.Stop(); err != nil { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.dev.stop_error", map[string]interface{}{"Error": err})) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.dev.stop_error", map[string]interface{}{"Error": err})) } - fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) + cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) return nil } @@ -217,7 +217,7 @@ func runPHPLogs(service string, follow bool) error { logsReader, err := server.Logs(service, follow) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "logs"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "logs"), err) } defer logsReader.Close() @@ -264,16 +264,16 @@ func runPHPStop() error { return err } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.stop.stopping")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.stop.stopping")) // We need to find running processes // This is a simplified version - in practice you'd want to track PIDs server := NewDevServer(Options{Dir: cwd}) if err := server.Stop(); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.stop", "services"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.stop", "services"), err) } - fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) + cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.dev.all_stopped")) return nil } @@ -304,24 +304,24 @@ func runPHPStatus() error { appName = "Laravel" } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("project")), appName) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("project")), appName) // Detect available services services := DetectServices(cwd) - fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services"))) + cli.Print("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services"))) for _, svc := range services { style := getServiceStyle(string(svc)) - fmt.Printf(" %s %s\n", style.Render("*"), svc) + cli.Print(" %s %s\n", style.Render("*"), svc) } - fmt.Println() + cli.Line("") // Package manager pm := DetectPackageManager(cwd) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm) // FrankenPHP status if IsFrankenPHPProject(cwd) { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP") + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP") } // SSL status @@ -329,9 +329,9 @@ func runPHPStatus() error { if appURL != "" { domain := ExtractDomainFromURL(appURL) if CertsExist(domain, SSLOptions{}) { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed"))) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed"))) } else { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup"))) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup"))) } } @@ -373,35 +373,35 @@ func runPHPSSL(domain string) error { // Check if mkcert is installed if !IsMkcertInstalled() { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.ssl.mkcert_not_installed")) - fmt.Printf("\n%s\n", i18n.T("common.hint.install_with")) - fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_macos")) - fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux")) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.ssl.mkcert_not_installed")) + cli.Print("\n%s\n", i18n.T("common.hint.install_with")) + cli.Print(" %s\n", i18n.T("cmd.php.ssl.install_macos")) + cli.Print(" %s\n", i18n.T("cmd.php.ssl.install_linux")) return errors.New(i18n.T("cmd.php.error.mkcert_not_installed")) } - fmt.Printf("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain})) + cli.Print("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain})) // Check if certs already exist if CertsExist(domain, SSLOptions{}) { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("skip")), i18n.T("cmd.php.ssl.certs_exist")) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("skip")), i18n.T("cmd.php.ssl.certs_exist")) certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) return nil } // Setup SSL if err := SetupSSL(domain, SSLOptions{}); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.setup", "SSL"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.setup", "SSL"), err) } certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) - fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.ssl.certs_created")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) + cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.ssl.certs_created")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) return nil } @@ -418,16 +418,16 @@ func printServiceStatuses(statuses []ServiceStatus) { } else if s.Running { statusText = phpStatusRunning.Render(i18n.T("cmd.php.status.running")) if s.Port > 0 { - statusText += dimStyle.Render(fmt.Sprintf(" (%s)", i18n.T("cmd.php.status.port", map[string]interface{}{"Port": s.Port}))) + statusText += dimStyle.Render(cli.Sprintf(" (%s)", i18n.T("cmd.php.status.port", map[string]interface{}{"Port": s.Port}))) } if s.PID > 0 { - statusText += dimStyle.Render(fmt.Sprintf(" [%s]", i18n.T("cmd.php.status.pid", map[string]interface{}{"PID": s.PID}))) + statusText += dimStyle.Render(cli.Sprintf(" [%s]", i18n.T("cmd.php.status.pid", map[string]interface{}{"PID": s.PID}))) } } else { statusText = phpStatusStopped.Render(i18n.T("cmd.php.status.stopped")) } - fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText) + cli.Print(" %s %s\n", style.Render(s.Name+":"), statusText) } } @@ -460,13 +460,13 @@ func printColoredLog(line string) { line = strings.TrimPrefix(line, "[Redis] ") } else { // Unknown service, print as-is - fmt.Printf("%s %s\n", dimStyle.Render(timestamp), line) + cli.Print("%s %s\n", dimStyle.Render(timestamp), line) return } - fmt.Printf("%s %s %s\n", + cli.Print("%s %s %s\n", dimStyle.Render(timestamp), - style.Render(fmt.Sprintf("[%s]", serviceName)), + style.Render(cli.Sprintf("[%s]", serviceName)), line, ) } diff --git a/pkg/php/cmd_packages.go b/pkg/php/cmd_packages.go index f01a5688..32ddc9f6 100644 --- a/pkg/php/cmd_packages.go +++ b/pkg/php/cmd_packages.go @@ -1,9 +1,9 @@ package php import ( - "fmt" "os" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" ) @@ -31,16 +31,16 @@ func addPHPPackagesLinkCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking")) if err := LinkPackages(cwd, args); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.link", "packages"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.link", "packages"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.link.done")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.link.done")) return nil }, } @@ -57,16 +57,16 @@ func addPHPPackagesUnlinkCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking")) if err := UnlinkPackages(cwd, args); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.unlink", "packages"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.unlink", "packages"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.unlink.done")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.unlink.done")) return nil }, } @@ -82,16 +82,16 @@ func addPHPPackagesUpdateCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating")) if err := UpdatePackages(cwd, args); err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.update_packages"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.update.done")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.packages.update.done")) return nil }, } @@ -107,20 +107,20 @@ func addPHPPackagesListCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } packages, err := ListLinkedPackages(cwd) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.list", "packages"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.list", "packages"), err) } if len(packages) == 0 { - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.none_found")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.none_found")) return nil } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.linked")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.linked")) for _, pkg := range packages { name := pkg.Name @@ -132,10 +132,10 @@ func addPHPPackagesListCommand(parent *cobra.Command) { version = "dev" } - fmt.Printf(" %s %s\n", successStyle.Render("*"), name) - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("path")), pkg.Path) - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("version")), version) - fmt.Println() + cli.Print(" %s %s\n", successStyle.Render("*"), name) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("path")), pkg.Path) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("version")), version) + cli.Line("") } return nil diff --git a/pkg/php/cmd_qa_runner.go b/pkg/php/cmd_qa_runner.go index b284a843..9d8c8ce4 100644 --- a/pkg/php/cmd_qa_runner.go +++ b/pkg/php/cmd_qa_runner.go @@ -2,12 +2,12 @@ package php import ( "context" - "fmt" "os" "path/filepath" "strings" "sync" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/process" @@ -32,12 +32,12 @@ func NewQARunner(dir string, fix bool) (*QARunner, error) { framework.WithName("process", process.NewService(process.Options{})), ) if err != nil { - return nil, fmt.Errorf("failed to create process service: %w", err) + return nil, cli.WrapVerb(err, "create", "process service") } svc, err := framework.ServiceFor[*process.Service](core, "process") if err != nil { - return nil, fmt.Errorf("failed to get process service: %w", err) + return nil, cli.WrapVerb(err, "get", "process service") } runner := &QARunner{ diff --git a/pkg/php/cmd_quality.go b/pkg/php/cmd_quality.go index 7b073b83..9de6524d 100644 --- a/pkg/php/cmd_quality.go +++ b/pkg/php/cmd_quality.go @@ -3,11 +3,11 @@ package php import ( "context" "errors" - "fmt" "os" "strings" "github.com/charmbracelet/lipgloss" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" ) @@ -27,14 +27,14 @@ func addPHPTestCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { return errors.New(i18n.T("cmd.php.error.not_php")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests")) ctx := context.Background() @@ -51,7 +51,7 @@ func addPHPTestCommand(parent *cobra.Command) { } if err := RunTests(ctx, opts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.run", "tests"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.run", "tests"), err) } return nil @@ -79,7 +79,7 @@ func addPHPFmtCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -98,7 +98,7 @@ func addPHPFmtCommand(parent *cobra.Command) { } else { msg = i18n.ProgressSubject("check", "code style") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg) ctx := context.Background() @@ -116,15 +116,15 @@ func addPHPFmtCommand(parent *cobra.Command) { if err := Format(ctx, opts); err != nil { if fmtFix { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err) } - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err) } if fmtFix { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"})) } else { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues")) } return nil @@ -146,11 +146,11 @@ func addPHPStanCommand(parent *cobra.Command) { stanCmd := &cobra.Command{ Use: "stan [paths...]", Short: i18n.T("cmd.php.analyse.short"), - Long: i18n.T("cmd.php.analyse.long"), + Long: i18n.T("cmd.php.analyse.long"), RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -163,7 +163,7 @@ func addPHPStanCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.analyse.no_analyser")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis")) ctx := context.Background() @@ -180,10 +180,10 @@ func addPHPStanCommand(parent *cobra.Command) { } if err := Analyse(ctx, opts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) return nil }, } @@ -213,7 +213,7 @@ func addPHPPsalmCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -223,9 +223,9 @@ func addPHPPsalmCommand(parent *cobra.Command) { // Check if Psalm is available _, found := DetectPsalm(cwd) if !found { - fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.psalm.not_found")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.psalm.install")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup")) + cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.psalm.not_found")) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.psalm.install")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup")) return errors.New(i18n.T("cmd.php.error.psalm_not_installed")) } @@ -235,7 +235,7 @@ func addPHPPsalmCommand(parent *cobra.Command) { } else { msg = i18n.T("cmd.php.psalm.analysing") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg) ctx := context.Background() @@ -249,10 +249,10 @@ func addPHPPsalmCommand(parent *cobra.Command) { } if err := RunPsalm(ctx, opts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) return nil }, } @@ -278,14 +278,14 @@ func addPHPAuditCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { return errors.New(i18n.T("cmd.php.error.not_php")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning")) ctx := context.Background() @@ -296,7 +296,7 @@ func addPHPAuditCommand(parent *cobra.Command) { Output: os.Stdout, }) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.audit_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.audit_failed"), err) } // Print results @@ -317,7 +317,7 @@ func addPHPAuditCommand(parent *cobra.Command) { totalVulns += result.Vulnerabilities } - fmt.Printf(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status) + cli.Print(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status) // Show advisories for _, adv := range result.Advisories { @@ -326,18 +326,18 @@ func addPHPAuditCommand(parent *cobra.Command) { severity = "unknown" } sevStyle := getSeverityStyle(severity) - fmt.Printf(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package) + cli.Print(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package) if adv.Title != "" { - fmt.Printf(" %s\n", dimStyle.Render(adv.Title)) + cli.Print(" %s\n", dimStyle.Render(adv.Title)) } } } - fmt.Println() + cli.Line("") if totalVulns > 0 { - fmt.Printf("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns})) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("fix")), i18n.T("common.hint.fix_deps")) + cli.Print("%s %s\n", errorStyle.Render(i18n.Label("warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns})) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("fix")), i18n.T("common.hint.fix_deps")) return errors.New(i18n.T("cmd.php.error.vulns_found")) } @@ -345,7 +345,7 @@ func addPHPAuditCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.audit.completed_errors")) } - fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.audit.all_secure")) + cli.Print("%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.audit.all_secure")) return nil }, } @@ -371,14 +371,14 @@ func addPHPSecurityCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { return errors.New(i18n.T("cmd.php.error.not_php")) } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.ProgressSubject("run", "security checks")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.ProgressSubject("run", "security checks")) ctx := context.Background() @@ -391,7 +391,7 @@ func addPHPSecurityCommand(parent *cobra.Command) { Output: os.Stdout, }) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.security_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.security_failed"), err) } // Print results by category @@ -400,10 +400,10 @@ func addPHPSecurityCommand(parent *cobra.Command) { category := strings.Split(check.ID, "_")[0] if category != currentCategory { if currentCategory != "" { - fmt.Println() + cli.Line("") } currentCategory = category - fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix"))) + cli.Print(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix"))) } icon := successStyle.Render("✓") @@ -411,32 +411,32 @@ func addPHPSecurityCommand(parent *cobra.Command) { icon = getSeverityStyle(check.Severity).Render("✗") } - fmt.Printf(" %s %s\n", icon, check.Name) + cli.Print(" %s %s\n", icon, check.Name) if !check.Passed && check.Message != "" { - fmt.Printf(" %s\n", dimStyle.Render(check.Message)) + cli.Print(" %s\n", dimStyle.Render(check.Message)) if check.Fix != "" { - fmt.Printf(" %s %s\n", dimStyle.Render(i18n.Label("fix")), check.Fix) + cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("fix")), check.Fix) } } } - fmt.Println() + cli.Line("") // Print summary - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.php.security.summary")) - fmt.Printf(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.php.security.summary")) + cli.Print(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total) if result.Summary.Critical > 0 { - fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical) + cli.Print(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical) } if result.Summary.High > 0 { - fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High) + cli.Print(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High) } if result.Summary.Medium > 0 { - fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium) + cli.Print(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium) } if result.Summary.Low > 0 { - fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low) + cli.Print(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low) } if result.Summary.Critical > 0 || result.Summary.High > 0 { @@ -469,7 +469,7 @@ func addPHPQACommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -486,20 +486,20 @@ func addPHPQACommand(parent *cobra.Command) { stages := GetQAStages(opts) // Print header - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) ctx := context.Background() // Create QA runner using pkg/process runner, err := NewQARunner(cwd, qaFix) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.create", "QA runner"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.create", "QA runner"), err) } // Run all checks with dependency ordering result, err := runner.Run(ctx, stages) if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err) } // Display results by stage @@ -509,10 +509,10 @@ func addPHPQACommand(parent *cobra.Command) { stage := getCheckStage(checkResult.Name, stages, cwd) if stage != currentStage { if currentStage != "" { - fmt.Println() + cli.Line("") } currentStage = stage - fmt.Printf("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──")) + cli.Print("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──")) } icon := phpQAPassedStyle.Render("✓") @@ -525,21 +525,21 @@ func addPHPQACommand(parent *cobra.Command) { status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail")) } - fmt.Printf(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration)) + cli.Print(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration)) } - fmt.Println() + cli.Line("") // Print summary if result.Passed { - fmt.Printf("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration) + cli.Print("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration) return nil } - fmt.Printf("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+fmt.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass")) + cli.Print("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+cli.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass")) // Show what needs fixing - fmt.Printf("%s\n", dimStyle.Render(i18n.T("i18n.label.fix"))) + cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix"))) for _, checkResult := range result.Results { if checkResult.Passed || checkResult.Skipped { continue @@ -549,13 +549,13 @@ func addPHPQACommand(parent *cobra.Command) { if issue == "" { issue = "issues found" } - fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue) + cli.Print(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue) if fixCmd != "" { - fmt.Printf(" %s %s\n", dimStyle.Render("->"), fixCmd) + cli.Print(" %s %s\n", dimStyle.Render("->"), fixCmd) } } - return fmt.Errorf("%s", i18n.T("i18n.fail.run", "QA pipeline")) + return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) }, } @@ -619,7 +619,7 @@ func addPHPRectorCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -628,9 +628,9 @@ func addPHPRectorCommand(parent *cobra.Command) { // Check if Rector is available if !DetectRector(cwd) { - fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.rector.not_found")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.rector.install")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup")) + cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.rector.not_found")) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.rector.install")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup")) return errors.New(i18n.T("cmd.php.error.rector_not_installed")) } @@ -640,7 +640,7 @@ func addPHPRectorCommand(parent *cobra.Command) { } else { msg = i18n.T("cmd.php.rector.analysing") } - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg) ctx := context.Background() @@ -654,17 +654,17 @@ func addPHPRectorCommand(parent *cobra.Command) { if err := RunRector(ctx, opts); err != nil { if rectorFix { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.rector_failed"), err) } // Dry-run returns non-zero if changes would be made - fmt.Printf("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested")) + cli.Print("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested")) return nil } if rectorFix { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code refactored"})) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code refactored"})) } else { - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.rector.no_changes")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.rector.no_changes")) } return nil }, @@ -693,7 +693,7 @@ func addPHPInfectionCommand(parent *cobra.Command) { RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) + return cli.Err("%s: %w", i18n.T("i18n.fail.get", "working directory"), err) } if !IsPHPProject(cwd) { @@ -702,13 +702,13 @@ func addPHPInfectionCommand(parent *cobra.Command) { // Check if Infection is available if !DetectInfection(cwd) { - fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.infection.not_found")) - fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.infection.install")) + cli.Print("%s %s\n\n", errorStyle.Render(i18n.Label("error")), i18n.T("cmd.php.infection.not_found")) + cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.T("cmd.php.infection.install")) return errors.New(i18n.T("cmd.php.error.infection_not_installed")) } - fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.ProgressSubject("run", "mutation testing")) - fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.ProgressSubject("run", "mutation testing")) + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note")) ctx := context.Background() @@ -723,10 +723,10 @@ func addPHPInfectionCommand(parent *cobra.Command) { } if err := RunInfection(ctx, opts); err != nil { - return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err) + return cli.Err("%s: %w", i18n.T("cmd.php.error.infection_failed"), err) } - fmt.Printf("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.infection.complete")) + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.infection.complete")) return nil }, } diff --git a/pkg/php/container.go b/pkg/php/container.go index 5f37ae6b..b14916c2 100644 --- a/pkg/php/container.go +++ b/pkg/php/container.go @@ -2,12 +2,13 @@ package php import ( "context" - "fmt" "io" "os" "os/exec" "path/filepath" "strings" + + "github.com/host-uk/core/pkg/cli" ) // DockerBuildOptions configures Docker image building for PHP projects. @@ -94,14 +95,14 @@ func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { if opts.ProjectDir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.ProjectDir = cwd } // Validate project directory if !IsPHPProject(opts.ProjectDir) { - return fmt.Errorf("not a PHP project: %s (missing composer.json)", opts.ProjectDir) + return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) } // Set defaults @@ -123,13 +124,13 @@ func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { // Generate Dockerfile content, err := GenerateDockerfile(opts.ProjectDir) if err != nil { - return fmt.Errorf("failed to generate Dockerfile: %w", err) + return cli.WrapVerb(err, "generate", "Dockerfile") } // Write to temporary file tempDockerfile = filepath.Join(opts.ProjectDir, "Dockerfile.core-generated") if err := os.WriteFile(tempDockerfile, []byte(content), 0644); err != nil { - return fmt.Errorf("failed to write Dockerfile: %w", err) + return cli.WrapVerb(err, "write", "Dockerfile") } defer os.Remove(tempDockerfile) @@ -137,7 +138,7 @@ func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { } // Build Docker image - imageRef := fmt.Sprintf("%s:%s", opts.ImageName, opts.Tag) + imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) args := []string{"build", "-t", imageRef, "-f", dockerfilePath} @@ -150,7 +151,7 @@ func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { } for key, value := range opts.BuildArgs { - args = append(args, "--build-arg", fmt.Sprintf("%s=%s", key, value)) + args = append(args, "--build-arg", cli.Sprintf("%s=%s", key, value)) } args = append(args, opts.ProjectDir) @@ -161,7 +162,7 @@ func BuildDocker(ctx context.Context, opts DockerBuildOptions) error { cmd.Stderr = opts.Output if err := cmd.Run(); err != nil { - return fmt.Errorf("docker build failed: %w", err) + return cli.Wrap(err, "docker build failed") } return nil @@ -172,14 +173,14 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { if opts.ProjectDir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.ProjectDir = cwd } // Validate project directory if !IsPHPProject(opts.ProjectDir) { - return fmt.Errorf("not a PHP project: %s (missing composer.json)", opts.ProjectDir) + return cli.Err("not a PHP project: %s (missing composer.json)", opts.ProjectDir) } // Set defaults @@ -199,7 +200,7 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { // Ensure output directory exists outputDir := filepath.Dir(opts.OutputPath) if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) + return cli.WrapVerb(err, "create", "output directory") } // Find linuxkit binary @@ -211,7 +212,7 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { // Get template content templateContent, err := getLinuxKitTemplate(opts.Template) if err != nil { - return fmt.Errorf("failed to get template: %w", err) + return cli.WrapVerb(err, "get", "template") } // Apply variables @@ -224,13 +225,13 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { content, err := applyTemplateVariables(templateContent, opts.Variables) if err != nil { - return fmt.Errorf("failed to apply template variables: %w", err) + return cli.WrapVerb(err, "apply", "template variables") } // Write template to temp file tempYAML := filepath.Join(opts.ProjectDir, ".core-linuxkit.yml") if err := os.WriteFile(tempYAML, []byte(content), 0644); err != nil { - return fmt.Errorf("failed to write template: %w", err) + return cli.WrapVerb(err, "write", "template") } defer os.Remove(tempYAML) @@ -248,7 +249,7 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { cmd.Stderr = opts.Output if err := cmd.Run(); err != nil { - return fmt.Errorf("linuxkit build failed: %w", err) + return cli.Wrap(err, "linuxkit build failed") } return nil @@ -257,7 +258,7 @@ func BuildLinuxKit(ctx context.Context, opts LinuxKitBuildOptions) error { // ServeProduction runs a production PHP container. func ServeProduction(ctx context.Context, opts ServeOptions) error { if opts.ImageName == "" { - return fmt.Errorf("image name is required") + return cli.Err("image name is required") } // Set defaults @@ -274,7 +275,7 @@ func ServeProduction(ctx context.Context, opts ServeOptions) error { opts.Output = os.Stdout } - imageRef := fmt.Sprintf("%s:%s", opts.ImageName, opts.Tag) + imageRef := cli.Sprintf("%s:%s", opts.ImageName, opts.Tag) args := []string{"run"} @@ -289,8 +290,8 @@ func ServeProduction(ctx context.Context, opts ServeOptions) error { } // Port mappings - args = append(args, "-p", fmt.Sprintf("%d:80", opts.Port)) - args = append(args, "-p", fmt.Sprintf("%d:443", opts.HTTPSPort)) + args = append(args, "-p", cli.Sprintf("%d:80", opts.Port)) + args = append(args, "-p", cli.Sprintf("%d:443", opts.HTTPSPort)) // Environment file if opts.EnvFile != "" { @@ -299,7 +300,7 @@ func ServeProduction(ctx context.Context, opts ServeOptions) error { // Volume mounts for hostPath, containerPath := range opts.Volumes { - args = append(args, "-v", fmt.Sprintf("%s:%s", hostPath, containerPath)) + args = append(args, "-v", cli.Sprintf("%s:%s", hostPath, containerPath)) } args = append(args, imageRef) @@ -311,10 +312,10 @@ func ServeProduction(ctx context.Context, opts ServeOptions) error { if opts.Detach { output, err := cmd.Output() if err != nil { - return fmt.Errorf("failed to start container: %w", err) + return cli.WrapVerb(err, "start", "container") } containerID := strings.TrimSpace(string(output)) - fmt.Fprintf(opts.Output, "Container started: %s\n", containerID[:12]) + cli.Print("Container started: %s\n", containerID[:12]) return nil } @@ -324,7 +325,7 @@ func ServeProduction(ctx context.Context, opts ServeOptions) error { // Shell opens a shell in a running container. func Shell(ctx context.Context, containerID string) error { if containerID == "" { - return fmt.Errorf("container ID is required") + return cli.Err("container ID is required") } // Resolve partial container ID @@ -367,7 +368,7 @@ func lookupLinuxKit() (string, error) { } } - return "", fmt.Errorf("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit") + return "", cli.Err("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit") } // getLinuxKitTemplate retrieves a LinuxKit template by name. @@ -379,7 +380,7 @@ func getLinuxKitTemplate(name string) (string, error) { // Try to load from container package templates // This would integrate with github.com/host-uk/core/pkg/container - return "", fmt.Errorf("template not found: %s", name) + return "", cli.Err("template not found: %s", name) } // applyTemplateVariables applies variable substitution to template content. @@ -397,7 +398,7 @@ func resolveDockerContainerID(ctx context.Context, partialID string) (string, er cmd := exec.CommandContext(ctx, "docker", "ps", "-a", "--no-trunc", "--format", "{{.ID}}") output, err := cmd.Output() if err != nil { - return "", fmt.Errorf("failed to list containers: %w", err) + return "", cli.WrapVerb(err, "list", "containers") } lines := strings.Split(strings.TrimSpace(string(output)), "\n") @@ -411,11 +412,11 @@ func resolveDockerContainerID(ctx context.Context, partialID string) (string, er switch len(matches) { case 0: - return "", fmt.Errorf("no container found matching: %s", partialID) + return "", cli.Err("no container found matching: %s", partialID) case 1: return matches[0], nil default: - return "", fmt.Errorf("multiple containers match '%s', be more specific", partialID) + return "", cli.Err("multiple containers match '%s', be more specific", partialID) } } diff --git a/pkg/php/coolify.go b/pkg/php/coolify.go index 2a83e77f..fe2e59bc 100644 --- a/pkg/php/coolify.go +++ b/pkg/php/coolify.go @@ -4,13 +4,14 @@ import ( "bytes" "context" "encoding/json" - "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" + + "github.com/host-uk/core/pkg/cli" ) // CoolifyClient is an HTTP client for the Coolify API. @@ -89,13 +90,13 @@ func LoadCoolifyConfigFromFile(path string) (*CoolifyConfig, error) { // No .env file, just use env vars return validateCoolifyConfig(config) } - return nil, fmt.Errorf("failed to open .env file: %w", err) + return nil, cli.WrapVerb(err, "open", ".env file") } defer file.Close() content, err := io.ReadAll(file) if err != nil { - return nil, fmt.Errorf("failed to read .env file: %w", err) + return nil, cli.WrapVerb(err, "read", ".env file") } // Parse .env file @@ -143,17 +144,17 @@ func LoadCoolifyConfigFromFile(path string) (*CoolifyConfig, error) { // validateCoolifyConfig checks that required fields are set. func validateCoolifyConfig(config *CoolifyConfig) (*CoolifyConfig, error) { if config.URL == "" { - return nil, fmt.Errorf("COOLIFY_URL is not set") + return nil, cli.Err("COOLIFY_URL is not set") } if config.Token == "" { - return nil, fmt.Errorf("COOLIFY_TOKEN is not set") + return nil, cli.Err("COOLIFY_TOKEN is not set") } return config, nil } // TriggerDeploy triggers a deployment for the specified application. func (c *CoolifyClient) TriggerDeploy(ctx context.Context, appID string, force bool) (*CoolifyDeployment, error) { - endpoint := fmt.Sprintf("%s/api/v1/applications/%s/deploy", c.BaseURL, appID) + endpoint := cli.Sprintf("%s/api/v1/applications/%s/deploy", c.BaseURL, appID) payload := map[string]interface{}{} if force { @@ -162,19 +163,19 @@ func (c *CoolifyClient) TriggerDeploy(ctx context.Context, appID string, force b body, err := json.Marshal(payload) if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) + return nil, cli.WrapVerb(err, "marshal", "request") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, cli.WrapVerb(err, "create", "request") } c.setHeaders(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, cli.Wrap(err, "request failed") } defer resp.Body.Close() @@ -196,18 +197,18 @@ func (c *CoolifyClient) TriggerDeploy(ctx context.Context, appID string, force b // GetDeployment retrieves a specific deployment by ID. func (c *CoolifyClient) GetDeployment(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) { - endpoint := fmt.Sprintf("%s/api/v1/applications/%s/deployments/%s", c.BaseURL, appID, deploymentID) + endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments/%s", c.BaseURL, appID, deploymentID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, cli.WrapVerb(err, "create", "request") } c.setHeaders(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, cli.Wrap(err, "request failed") } defer resp.Body.Close() @@ -217,7 +218,7 @@ func (c *CoolifyClient) GetDeployment(ctx context.Context, appID, deploymentID s var deployment CoolifyDeployment if err := json.NewDecoder(resp.Body).Decode(&deployment); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) + return nil, cli.WrapVerb(err, "decode", "response") } return &deployment, nil @@ -225,21 +226,21 @@ func (c *CoolifyClient) GetDeployment(ctx context.Context, appID, deploymentID s // ListDeployments retrieves deployments for an application. func (c *CoolifyClient) ListDeployments(ctx context.Context, appID string, limit int) ([]CoolifyDeployment, error) { - endpoint := fmt.Sprintf("%s/api/v1/applications/%s/deployments", c.BaseURL, appID) + endpoint := cli.Sprintf("%s/api/v1/applications/%s/deployments", c.BaseURL, appID) if limit > 0 { - endpoint = fmt.Sprintf("%s?limit=%d", endpoint, limit) + endpoint = cli.Sprintf("%s?limit=%d", endpoint, limit) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, cli.WrapVerb(err, "create", "request") } c.setHeaders(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, cli.Wrap(err, "request failed") } defer resp.Body.Close() @@ -249,7 +250,7 @@ func (c *CoolifyClient) ListDeployments(ctx context.Context, appID string, limit var deployments []CoolifyDeployment if err := json.NewDecoder(resp.Body).Decode(&deployments); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) + return nil, cli.WrapVerb(err, "decode", "response") } return deployments, nil @@ -257,7 +258,7 @@ func (c *CoolifyClient) ListDeployments(ctx context.Context, appID string, limit // Rollback triggers a rollback to a previous deployment. func (c *CoolifyClient) Rollback(ctx context.Context, appID, deploymentID string) (*CoolifyDeployment, error) { - endpoint := fmt.Sprintf("%s/api/v1/applications/%s/rollback", c.BaseURL, appID) + endpoint := cli.Sprintf("%s/api/v1/applications/%s/rollback", c.BaseURL, appID) payload := map[string]interface{}{ "deployment_id": deploymentID, @@ -265,19 +266,19 @@ func (c *CoolifyClient) Rollback(ctx context.Context, appID, deploymentID string body, err := json.Marshal(payload) if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) + return nil, cli.WrapVerb(err, "marshal", "request") } req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, cli.WrapVerb(err, "create", "request") } c.setHeaders(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, cli.Wrap(err, "request failed") } defer resp.Body.Close() @@ -298,18 +299,18 @@ func (c *CoolifyClient) Rollback(ctx context.Context, appID, deploymentID string // GetApp retrieves application details. func (c *CoolifyClient) GetApp(ctx context.Context, appID string) (*CoolifyApp, error) { - endpoint := fmt.Sprintf("%s/api/v1/applications/%s", c.BaseURL, appID) + endpoint := cli.Sprintf("%s/api/v1/applications/%s", c.BaseURL, appID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, cli.WrapVerb(err, "create", "request") } c.setHeaders(req) resp, err := c.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("request failed: %w", err) + return nil, cli.Wrap(err, "request failed") } defer resp.Body.Close() @@ -319,7 +320,7 @@ func (c *CoolifyClient) GetApp(ctx context.Context, appID string) (*CoolifyApp, var app CoolifyApp if err := json.NewDecoder(resp.Body).Decode(&app); err != nil { - return nil, fmt.Errorf("failed to decode response: %w", err) + return nil, cli.WrapVerb(err, "decode", "response") } return &app, nil @@ -343,12 +344,12 @@ func (c *CoolifyClient) parseError(resp *http.Response) error { if err := json.Unmarshal(body, &errResp); err == nil { if errResp.Message != "" { - return fmt.Errorf("API error (%d): %s", resp.StatusCode, errResp.Message) + return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Message) } if errResp.Error != "" { - return fmt.Errorf("API error (%d): %s", resp.StatusCode, errResp.Error) + return cli.Err("API error (%d): %s", resp.StatusCode, errResp.Error) } } - return fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body)) + return cli.Err("API error (%d): %s", resp.StatusCode, string(body)) } diff --git a/pkg/php/deploy.go b/pkg/php/deploy.go index fc59b6e7..220c262b 100644 --- a/pkg/php/deploy.go +++ b/pkg/php/deploy.go @@ -2,8 +2,9 @@ package php import ( "context" - "fmt" "time" + + "github.com/host-uk/core/pkg/cli" ) // Environment represents a deployment environment. @@ -120,13 +121,13 @@ func Deploy(ctx context.Context, opts DeployOptions) (*DeploymentStatus, error) // Load config config, err := LoadCoolifyConfig(opts.Dir) if err != nil { - return nil, fmt.Errorf("failed to load Coolify config: %w", err) + return nil, cli.WrapVerb(err, "load", "Coolify config") } // Get app ID for environment appID := getAppIDForEnvironment(config, opts.Environment) if appID == "" { - return nil, fmt.Errorf("no app ID configured for %s environment", opts.Environment) + return nil, cli.Err("no app ID configured for %s environment", opts.Environment) } // Create client @@ -135,7 +136,7 @@ func Deploy(ctx context.Context, opts DeployOptions) (*DeploymentStatus, error) // Trigger deployment deployment, err := client.TriggerDeploy(ctx, appID, opts.Force) if err != nil { - return nil, fmt.Errorf("failed to trigger deployment: %w", err) + return nil, cli.WrapVerb(err, "trigger", "deployment") } status := convertDeployment(deployment) @@ -169,13 +170,13 @@ func DeployStatus(ctx context.Context, opts StatusOptions) (*DeploymentStatus, e // Load config config, err := LoadCoolifyConfig(opts.Dir) if err != nil { - return nil, fmt.Errorf("failed to load Coolify config: %w", err) + return nil, cli.WrapVerb(err, "load", "Coolify config") } // Get app ID for environment appID := getAppIDForEnvironment(config, opts.Environment) if appID == "" { - return nil, fmt.Errorf("no app ID configured for %s environment", opts.Environment) + return nil, cli.Err("no app ID configured for %s environment", opts.Environment) } // Create client @@ -187,16 +188,16 @@ func DeployStatus(ctx context.Context, opts StatusOptions) (*DeploymentStatus, e // Get specific deployment deployment, err = client.GetDeployment(ctx, appID, opts.DeploymentID) if err != nil { - return nil, fmt.Errorf("failed to get deployment: %w", err) + return nil, cli.WrapVerb(err, "get", "deployment") } } else { // Get latest deployment deployments, err := client.ListDeployments(ctx, appID, 1) if err != nil { - return nil, fmt.Errorf("failed to list deployments: %w", err) + return nil, cli.WrapVerb(err, "list", "deployments") } if len(deployments) == 0 { - return nil, fmt.Errorf("no deployments found") + return nil, cli.Err("no deployments found") } deployment = &deployments[0] } @@ -227,13 +228,13 @@ func Rollback(ctx context.Context, opts RollbackOptions) (*DeploymentStatus, err // Load config config, err := LoadCoolifyConfig(opts.Dir) if err != nil { - return nil, fmt.Errorf("failed to load Coolify config: %w", err) + return nil, cli.WrapVerb(err, "load", "Coolify config") } // Get app ID for environment appID := getAppIDForEnvironment(config, opts.Environment) if appID == "" { - return nil, fmt.Errorf("no app ID configured for %s environment", opts.Environment) + return nil, cli.Err("no app ID configured for %s environment", opts.Environment) } // Create client @@ -245,7 +246,7 @@ func Rollback(ctx context.Context, opts RollbackOptions) (*DeploymentStatus, err // Find previous successful deployment deployments, err := client.ListDeployments(ctx, appID, 10) if err != nil { - return nil, fmt.Errorf("failed to list deployments: %w", err) + return nil, cli.WrapVerb(err, "list", "deployments") } // Skip the first (current) deployment, find the last successful one @@ -260,14 +261,14 @@ func Rollback(ctx context.Context, opts RollbackOptions) (*DeploymentStatus, err } if deploymentID == "" { - return nil, fmt.Errorf("no previous successful deployment found to rollback to") + return nil, cli.Err("no previous successful deployment found to rollback to") } } // Trigger rollback deployment, err := client.Rollback(ctx, appID, deploymentID) if err != nil { - return nil, fmt.Errorf("failed to trigger rollback: %w", err) + return nil, cli.WrapVerb(err, "trigger", "rollback") } status := convertDeployment(deployment) @@ -298,13 +299,13 @@ func ListDeployments(ctx context.Context, dir string, env Environment, limit int // Load config config, err := LoadCoolifyConfig(dir) if err != nil { - return nil, fmt.Errorf("failed to load Coolify config: %w", err) + return nil, cli.WrapVerb(err, "load", "Coolify config") } // Get app ID for environment appID := getAppIDForEnvironment(config, env) if appID == "" { - return nil, fmt.Errorf("no app ID configured for %s environment", env) + return nil, cli.Err("no app ID configured for %s environment", env) } // Create client @@ -312,7 +313,7 @@ func ListDeployments(ctx context.Context, dir string, env Environment, limit int deployments, err := client.ListDeployments(ctx, appID, limit) if err != nil { - return nil, fmt.Errorf("failed to list deployments: %w", err) + return nil, cli.WrapVerb(err, "list", "deployments") } result := make([]DeploymentStatus, len(deployments)) @@ -364,7 +365,7 @@ func waitForDeployment(ctx context.Context, client *CoolifyClient, appID, deploy deployment, err := client.GetDeployment(ctx, appID, deploymentID) if err != nil { - return nil, fmt.Errorf("failed to get deployment status: %w", err) + return nil, cli.WrapVerb(err, "get", "deployment status") } status := convertDeployment(deployment) @@ -374,9 +375,9 @@ func waitForDeployment(ctx context.Context, client *CoolifyClient, appID, deploy case "finished", "success": return status, nil case "failed", "error": - return status, fmt.Errorf("deployment failed: %s", deployment.Status) + return status, cli.Err("deployment failed: %s", deployment.Status) case "cancelled": - return status, fmt.Errorf("deployment was cancelled") + return status, cli.Err("deployment was cancelled") } // Still in progress, wait and retry @@ -387,7 +388,7 @@ func waitForDeployment(ctx context.Context, client *CoolifyClient, appID, deploy } } - return nil, fmt.Errorf("deployment timed out after %v", timeout) + return nil, cli.Err("deployment timed out after %v", timeout) } // IsDeploymentComplete returns true if the status indicates completion. diff --git a/pkg/php/dockerfile.go b/pkg/php/dockerfile.go index eff11106..43a3b6cf 100644 --- a/pkg/php/dockerfile.go +++ b/pkg/php/dockerfile.go @@ -2,11 +2,12 @@ package php import ( "encoding/json" - "fmt" "os" "path/filepath" "sort" "strings" + + "github.com/host-uk/core/pkg/cli" ) // DockerfileConfig holds configuration for generating a Dockerfile. @@ -59,12 +60,12 @@ func DetectDockerfileConfig(dir string) (*DockerfileConfig, error) { composerPath := filepath.Join(dir, "composer.json") composerData, err := os.ReadFile(composerPath) if err != nil { - return nil, fmt.Errorf("failed to read composer.json: %w", err) + return nil, cli.WrapVerb(err, "read", "composer.json") } var composer ComposerJSON if err := json.Unmarshal(composerData, &composer); err != nil { - return nil, fmt.Errorf("failed to parse composer.json: %w", err) + return nil, cli.WrapVerb(err, "parse", "composer.json") } // Detect PHP version from composer.json @@ -99,13 +100,13 @@ func GenerateDockerfileFromConfig(config *DockerfileConfig) string { var sb strings.Builder // Base image - baseTag := fmt.Sprintf("latest-php%s", config.PHPVersion) + baseTag := cli.Sprintf("latest-php%s", config.PHPVersion) if config.UseAlpine { baseTag += "-alpine" } - sb.WriteString(fmt.Sprintf("# Auto-generated Dockerfile for FrankenPHP\n")) - sb.WriteString(fmt.Sprintf("# Generated by Core Framework\n\n")) + sb.WriteString("# Auto-generated Dockerfile for FrankenPHP\n") + sb.WriteString("# Generated by Core Framework\n\n") // Multi-stage build for smaller images if config.HasAssets { @@ -150,16 +151,16 @@ func GenerateDockerfileFromConfig(config *DockerfileConfig) string { // PHP build stage stageNum := 2 if config.HasAssets { - sb.WriteString(fmt.Sprintf("# Stage %d: PHP application\n", stageNum)) + sb.WriteString(cli.Sprintf("# Stage %d: PHP application\n", stageNum)) } - sb.WriteString(fmt.Sprintf("FROM %s:%s AS app\n\n", config.BaseImage, baseTag)) + sb.WriteString(cli.Sprintf("FROM %s:%s AS app\n\n", config.BaseImage, baseTag)) sb.WriteString("WORKDIR /app\n\n") // Install PHP extensions if needed if len(config.PHPExtensions) > 0 { sb.WriteString("# Install PHP extensions\n") - sb.WriteString(fmt.Sprintf("RUN install-php-extensions %s\n\n", strings.Join(config.PHPExtensions, " "))) + sb.WriteString(cli.Sprintf("RUN install-php-extensions %s\n\n", strings.Join(config.PHPExtensions, " "))) } // Copy composer files first for better caching @@ -231,27 +232,27 @@ func detectPHPExtensions(composer ComposerJSON) []string { // Check for common packages and their required extensions packageExtensions := map[string][]string{ // Database - "doctrine/dbal": {"pdo_mysql", "pdo_pgsql"}, - "illuminate/database": {"pdo_mysql"}, - "laravel/framework": {"pdo_mysql", "bcmath", "ctype", "fileinfo", "mbstring", "openssl", "tokenizer", "xml"}, - "mongodb/mongodb": {"mongodb"}, - "predis/predis": {"redis"}, - "phpredis/phpredis": {"redis"}, - "laravel/horizon": {"redis", "pcntl"}, - "aws/aws-sdk-php": {"curl"}, - "intervention/image": {"gd"}, + "doctrine/dbal": {"pdo_mysql", "pdo_pgsql"}, + "illuminate/database": {"pdo_mysql"}, + "laravel/framework": {"pdo_mysql", "bcmath", "ctype", "fileinfo", "mbstring", "openssl", "tokenizer", "xml"}, + "mongodb/mongodb": {"mongodb"}, + "predis/predis": {"redis"}, + "phpredis/phpredis": {"redis"}, + "laravel/horizon": {"redis", "pcntl"}, + "aws/aws-sdk-php": {"curl"}, + "intervention/image": {"gd"}, "intervention/image-laravel": {"gd"}, - "spatie/image": {"gd"}, + "spatie/image": {"gd"}, "league/flysystem-aws-s3-v3": {"curl"}, - "guzzlehttp/guzzle": {"curl"}, - "nelmio/cors-bundle": {}, + "guzzlehttp/guzzle": {"curl"}, + "nelmio/cors-bundle": {}, // Queues "laravel/reverb": {"pcntl"}, "php-amqplib/php-amqplib": {"sockets"}, // Misc - "moneyphp/money": {"bcmath", "intl"}, - "symfony/intl": {"intl"}, - "nesbot/carbon": {"intl"}, + "moneyphp/money": {"bcmath", "intl"}, + "symfony/intl": {"intl"}, + "nesbot/carbon": {"intl"}, "spatie/laravel-medialibrary": {"exif", "gd"}, } diff --git a/pkg/php/packages.go b/pkg/php/packages.go index e2c2df38..ba3501ff 100644 --- a/pkg/php/packages.go +++ b/pkg/php/packages.go @@ -2,10 +2,11 @@ package php import ( "encoding/json" - "fmt" "os" "os/exec" "path/filepath" + + "github.com/host-uk/core/pkg/cli" ) // LinkedPackage represents a linked local package. @@ -27,12 +28,12 @@ func readComposerJSON(dir string) (map[string]json.RawMessage, error) { composerPath := filepath.Join(dir, "composer.json") data, err := os.ReadFile(composerPath) if err != nil { - return nil, fmt.Errorf("failed to read composer.json: %w", err) + return nil, cli.WrapVerb(err, "read", "composer.json") } var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("failed to parse composer.json: %w", err) + return nil, cli.WrapVerb(err, "parse", "composer.json") } return raw, nil @@ -44,14 +45,14 @@ func writeComposerJSON(dir string, raw map[string]json.RawMessage) error { data, err := json.MarshalIndent(raw, "", " ") if err != nil { - return fmt.Errorf("failed to marshal composer.json: %w", err) + return cli.WrapVerb(err, "marshal", "composer.json") } // Add trailing newline data = append(data, '\n') if err := os.WriteFile(composerPath, data, 0644); err != nil { - return fmt.Errorf("failed to write composer.json: %w", err) + return cli.WrapVerb(err, "write", "composer.json") } return nil @@ -66,7 +67,7 @@ func getRepositories(raw map[string]json.RawMessage) ([]composerRepository, erro var repos []composerRepository if err := json.Unmarshal(reposRaw, &repos); err != nil { - return nil, fmt.Errorf("failed to parse repositories: %w", err) + return nil, cli.WrapVerb(err, "parse", "repositories") } return repos, nil @@ -81,7 +82,7 @@ func setRepositories(raw map[string]json.RawMessage, repos []composerRepository) reposData, err := json.Marshal(repos) if err != nil { - return fmt.Errorf("failed to marshal repositories: %w", err) + return cli.WrapVerb(err, "marshal", "repositories") } raw["repositories"] = reposData @@ -93,7 +94,7 @@ func getPackageInfo(packagePath string) (name, version string, err error) { composerPath := filepath.Join(packagePath, "composer.json") data, err := os.ReadFile(composerPath) if err != nil { - return "", "", fmt.Errorf("failed to read package composer.json: %w", err) + return "", "", cli.WrapVerb(err, "read", "package composer.json") } var pkg struct { @@ -102,11 +103,11 @@ func getPackageInfo(packagePath string) (name, version string, err error) { } if err := json.Unmarshal(data, &pkg); err != nil { - return "", "", fmt.Errorf("failed to parse package composer.json: %w", err) + return "", "", cli.WrapVerb(err, "parse", "package composer.json") } if pkg.Name == "" { - return "", "", fmt.Errorf("package name not found in composer.json") + return "", "", cli.Err("package name not found in composer.json") } return pkg.Name, pkg.Version, nil @@ -115,7 +116,7 @@ func getPackageInfo(packagePath string) (name, version string, err error) { // LinkPackages adds path repositories to composer.json for local package development. func LinkPackages(dir string, packages []string) error { if !IsPHPProject(dir) { - return fmt.Errorf("not a PHP project (missing composer.json)") + return cli.Err("not a PHP project (missing composer.json)") } raw, err := readComposerJSON(dir) @@ -132,18 +133,18 @@ func LinkPackages(dir string, packages []string) error { // Resolve absolute path absPath, err := filepath.Abs(packagePath) if err != nil { - return fmt.Errorf("failed to resolve path %s: %w", packagePath, err) + return cli.Err("failed to resolve path %s: %w", packagePath, err) } // Verify the path exists and has a composer.json if !IsPHPProject(absPath) { - return fmt.Errorf("not a PHP package (missing composer.json): %s", absPath) + return cli.Err("not a PHP package (missing composer.json): %s", absPath) } // Get package name for validation pkgName, _, err := getPackageInfo(absPath) if err != nil { - return fmt.Errorf("failed to get package info from %s: %w", absPath, err) + return cli.Err("failed to get package info from %s: %w", absPath, err) } // Check if already linked @@ -168,7 +169,7 @@ func LinkPackages(dir string, packages []string) error { }, }) - fmt.Printf("Linked: %s -> %s\n", pkgName, absPath) + cli.Print("Linked: %s -> %s\n", pkgName, absPath) } if err := setRepositories(raw, repos); err != nil { @@ -181,7 +182,7 @@ func LinkPackages(dir string, packages []string) error { // UnlinkPackages removes path repositories from composer.json. func UnlinkPackages(dir string, packages []string) error { if !IsPHPProject(dir) { - return fmt.Errorf("not a PHP project (missing composer.json)") + return cli.Err("not a PHP project (missing composer.json)") } raw, err := readComposerJSON(dir) @@ -216,7 +217,7 @@ func UnlinkPackages(dir string, packages []string) error { pkgName, _, err := getPackageInfo(repo.URL) if err == nil && toUnlink[pkgName] { shouldUnlink = true - fmt.Printf("Unlinked: %s\n", pkgName) + cli.Print("Unlinked: %s\n", pkgName) } } @@ -224,7 +225,7 @@ func UnlinkPackages(dir string, packages []string) error { for pkg := range toUnlink { if repo.URL == pkg || filepath.Base(repo.URL) == pkg { shouldUnlink = true - fmt.Printf("Unlinked: %s\n", repo.URL) + cli.Print("Unlinked: %s\n", repo.URL) break } } @@ -244,7 +245,7 @@ func UnlinkPackages(dir string, packages []string) error { // UpdatePackages runs composer update for specific packages. func UpdatePackages(dir string, packages []string) error { if !IsPHPProject(dir) { - return fmt.Errorf("not a PHP project (missing composer.json)") + return cli.Err("not a PHP project (missing composer.json)") } args := []string{"update"} @@ -261,7 +262,7 @@ func UpdatePackages(dir string, packages []string) error { // ListLinkedPackages returns all path repositories from composer.json. func ListLinkedPackages(dir string) ([]LinkedPackage, error) { if !IsPHPProject(dir) { - return nil, fmt.Errorf("not a PHP project (missing composer.json)") + return nil, cli.Err("not a PHP project (missing composer.json)") } raw, err := readComposerJSON(dir) diff --git a/pkg/php/php.go b/pkg/php/php.go index 07bcba62..c00b92e9 100644 --- a/pkg/php/php.go +++ b/pkg/php/php.go @@ -2,11 +2,12 @@ package php import ( "context" - "fmt" "io" "os" "sync" "time" + + "github.com/host-uk/core/pkg/cli" ) // Options configures the development server. @@ -69,7 +70,7 @@ func (d *DevServer) Start(ctx context.Context, opts Options) error { defer d.mu.Unlock() if d.running { - return fmt.Errorf("dev server is already running") + return cli.Err("dev server is already running") } // Merge options @@ -79,14 +80,14 @@ func (d *DevServer) Start(ctx context.Context, opts Options) error { if d.opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } d.opts.Dir = cwd } // Verify this is a Laravel project if !IsLaravelProject(d.opts.Dir) { - return fmt.Errorf("not a Laravel project: %s", d.opts.Dir) + return cli.Err("not a Laravel project: %s", d.opts.Dir) } // Create cancellable context @@ -119,7 +120,7 @@ func (d *DevServer) Start(ctx context.Context, opts Options) error { var err error certFile, keyFile, err = SetupSSLIfNeeded(domain, SSLOptions{}) if err != nil { - return fmt.Errorf("failed to setup SSL: %w", err) + return cli.WrapVerb(err, "setup", "SSL") } } @@ -187,7 +188,7 @@ func (d *DevServer) Start(ctx context.Context, opts Options) error { var startErrors []error for _, svc := range d.services { if err := svc.Start(d.ctx); err != nil { - startErrors = append(startErrors, fmt.Errorf("%s: %w", svc.Name(), err)) + startErrors = append(startErrors, cli.Err("%s: %v", svc.Name(), err)) } } @@ -196,7 +197,7 @@ func (d *DevServer) Start(ctx context.Context, opts Options) error { for _, svc := range d.services { svc.Stop() } - return fmt.Errorf("failed to start services: %v", startErrors) + return cli.Err("failed to start services: %v", startErrors) } d.running = true @@ -252,14 +253,14 @@ func (d *DevServer) Stop() error { for i := len(d.services) - 1; i >= 0; i-- { svc := d.services[i] if err := svc.Stop(); err != nil { - stopErrors = append(stopErrors, fmt.Errorf("%s: %w", svc.Name(), err)) + stopErrors = append(stopErrors, cli.Err("%s: %v", svc.Name(), err)) } } d.running = false if len(stopErrors) > 0 { - return fmt.Errorf("errors stopping services: %v", stopErrors) + return cli.Err("errors stopping services: %v", stopErrors) } return nil @@ -283,7 +284,7 @@ func (d *DevServer) Logs(service string, follow bool) (io.ReadCloser, error) { } } - return nil, fmt.Errorf("service not found: %s", service) + return nil, cli.Err("service not found: %s", service) } // unifiedLogs creates a reader that combines logs from all services. @@ -297,7 +298,7 @@ func (d *DevServer) unifiedLogs(follow bool) (io.ReadCloser, error) { for _, r := range readers { r.Close() } - return nil, fmt.Errorf("failed to get logs for %s: %w", svc.Name(), err) + return nil, cli.Err("failed to get logs for %s: %v", svc.Name(), err) } readers = append(readers, reader) } @@ -363,7 +364,7 @@ func (m *multiServiceReader) Read(p []byte) (n int, err error) { n, err := reader.Read(buf) if n > 0 { // Prefix with service name - prefix := fmt.Sprintf("[%s] ", m.services[i].Name()) + prefix := cli.Sprintf("[%s] ", m.services[i].Name()) copy(p, prefix) copy(p[len(prefix):], buf[:n]) return n + len(prefix), nil diff --git a/pkg/php/quality.go b/pkg/php/quality.go index e071a848..31c71cde 100644 --- a/pkg/php/quality.go +++ b/pkg/php/quality.go @@ -3,12 +3,13 @@ package php import ( "context" "encoding/json" - "fmt" "io" "os" "os/exec" "path/filepath" "strings" + + "github.com/host-uk/core/pkg/cli" ) // FormatOptions configures PHP code formatting. @@ -58,8 +59,8 @@ const ( type AnalyserType string const ( - AnalyserPHPStan AnalyserType = "phpstan" - AnalyserLarastan AnalyserType = "larastan" + AnalyserPHPStan AnalyserType = "phpstan" + AnalyserLarastan AnalyserType = "larastan" ) // DetectFormatter detects which formatter is available in the project. @@ -122,7 +123,7 @@ func Format(ctx context.Context, opts FormatOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -134,7 +135,7 @@ func Format(ctx context.Context, opts FormatOptions) error { // Check if formatter is available formatter, found := DetectFormatter(opts.Dir) if !found { - return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") + return cli.Err("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") } var cmdName string @@ -158,7 +159,7 @@ func Analyse(ctx context.Context, opts AnalyseOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -170,7 +171,7 @@ func Analyse(ctx context.Context, opts AnalyseOptions) error { // Check if analyser is available analyser, found := DetectAnalyser(opts.Dir) if !found { - return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") + return cli.Err("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") } var cmdName string @@ -226,7 +227,7 @@ func buildPHPStanCommand(opts AnalyseOptions) (string, []string) { args := []string{"analyse"} if opts.Level > 0 { - args = append(args, "--level", fmt.Sprintf("%d", opts.Level)) + args = append(args, "--level", cli.Sprintf("%d", opts.Level)) } if opts.Memory != "" { @@ -292,7 +293,7 @@ func RunPsalm(ctx context.Context, opts PsalmOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -311,7 +312,7 @@ func RunPsalm(ctx context.Context, opts PsalmOptions) error { args := []string{"--no-progress"} if opts.Level > 0 && opts.Level <= 8 { - args = append(args, fmt.Sprintf("--error-level=%d", opts.Level)) + args = append(args, cli.Sprintf("--error-level=%d", opts.Level)) } if opts.Fix { @@ -368,7 +369,7 @@ func RunAudit(ctx context.Context, opts AuditOptions) ([]AuditResult, error) { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return nil, fmt.Errorf("failed to get working directory: %w", err) + return nil, cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -520,7 +521,7 @@ func RunRector(ctx context.Context, opts RectorOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -597,7 +598,7 @@ func RunInfection(ctx context.Context, opts InfectionOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -629,9 +630,9 @@ func RunInfection(ctx context.Context, opts InfectionOptions) error { threads = 4 } - args = append(args, fmt.Sprintf("--min-msi=%d", minMSI)) - args = append(args, fmt.Sprintf("--min-covered-msi=%d", minCoveredMSI)) - args = append(args, fmt.Sprintf("--threads=%d", threads)) + args = append(args, cli.Sprintf("--min-msi=%d", minMSI)) + args = append(args, cli.Sprintf("--min-covered-msi=%d", minCoveredMSI)) + args = append(args, cli.Sprintf("--threads=%d", threads)) if opts.Filter != "" { args = append(args, "--filter="+opts.Filter) @@ -774,7 +775,7 @@ func RunSecurityChecks(ctx context.Context, opts SecurityOptions) (*SecurityResu if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return nil, fmt.Errorf("failed to get working directory: %w", err) + return nil, cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd } @@ -793,7 +794,7 @@ func RunSecurityChecks(ctx context.Context, opts SecurityOptions) (*SecurityResu CWE: "CWE-1395", } if !check.Passed { - check.Message = fmt.Sprintf("Found %d vulnerabilities", audit.Vulnerabilities) + check.Message = cli.Sprintf("Found %d vulnerabilities", audit.Vulnerabilities) } result.Checks = append(result.Checks, check) } diff --git a/pkg/php/services.go b/pkg/php/services.go index 6bea808c..44e0a61c 100644 --- a/pkg/php/services.go +++ b/pkg/php/services.go @@ -4,7 +4,6 @@ package php import ( "bufio" "context" - "fmt" "io" "os" "os/exec" @@ -13,6 +12,8 @@ import ( "sync" "syscall" "time" + + "github.com/host-uk/core/pkg/cli" ) // Service represents a managed development service. @@ -75,12 +76,12 @@ func (s *baseService) Status() ServiceStatus { func (s *baseService) Logs(follow bool) (io.ReadCloser, error) { if s.logPath == "" { - return nil, fmt.Errorf("no log file available for %s", s.name) + return nil, cli.Err("no log file available for %s", s.name) } file, err := os.Open(s.logPath) if err != nil { - return nil, fmt.Errorf("failed to open log file: %w", err) + return nil, cli.WrapVerb(err, "open", "log file") } if !follow { @@ -96,19 +97,19 @@ func (s *baseService) startProcess(ctx context.Context, cmdName string, args []s defer s.mu.Unlock() if s.running { - return fmt.Errorf("%s is already running", s.name) + return cli.Err("%s is already running", s.name) } // Create log file logDir := filepath.Join(s.dir, ".core", "logs") if err := os.MkdirAll(logDir, 0755); err != nil { - return fmt.Errorf("failed to create log directory: %w", err) + return cli.WrapVerb(err, "create", "log directory") } - s.logPath = filepath.Join(logDir, fmt.Sprintf("%s.log", strings.ToLower(s.name))) + s.logPath = filepath.Join(logDir, cli.Sprintf("%s.log", strings.ToLower(s.name))) logFile, err := os.OpenFile(s.logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - return fmt.Errorf("failed to create log file: %w", err) + return cli.WrapVerb(err, "create", "log file") } s.logFile = logFile @@ -127,7 +128,7 @@ func (s *baseService) startProcess(ctx context.Context, cmdName string, args []s if err := s.cmd.Start(); err != nil { logFile.Close() s.lastError = err - return fmt.Errorf("failed to start %s: %w", s.name, err) + return cli.WrapVerb(err, "start", s.name) } s.running = true @@ -192,10 +193,10 @@ func (s *baseService) stopProcess() error { // FrankenPHPService manages the FrankenPHP/Octane server. type FrankenPHPService struct { baseService - https bool + https bool httpsPort int - certFile string - keyFile string + certFile string + keyFile string } // NewFrankenPHPService creates a new FrankenPHP service. @@ -235,15 +236,15 @@ func (s *FrankenPHPService) Start(ctx context.Context) error { args := []string{ "artisan", "octane:start", "--server=frankenphp", - fmt.Sprintf("--port=%d", s.port), + cli.Sprintf("--port=%d", s.port), "--no-interaction", } if s.https && s.certFile != "" && s.keyFile != "" { args = append(args, - fmt.Sprintf("--https-port=%d", s.httpsPort), - fmt.Sprintf("--https-certificate=%s", s.certFile), - fmt.Sprintf("--https-certificate-key=%s", s.keyFile), + cli.Sprintf("--https-port=%d", s.httpsPort), + cli.Sprintf("--https-certificate=%s", s.certFile), + cli.Sprintf("--https-certificate-key=%s", s.keyFile), ) } @@ -372,7 +373,7 @@ type ReverbOptions struct { func (s *ReverbService) Start(ctx context.Context) error { args := []string{ "artisan", "reverb:start", - fmt.Sprintf("--port=%d", s.port), + cli.Sprintf("--port=%d", s.port), } return s.startProcess(ctx, "php", args, nil) @@ -413,13 +414,13 @@ type RedisOptions struct { func (s *RedisService) Start(ctx context.Context) error { args := []string{ - "--port", fmt.Sprintf("%d", s.port), + "--port", cli.Sprintf("%d", s.port), "--daemonize", "no", } if s.configFile != "" { args = []string{s.configFile} - args = append(args, "--port", fmt.Sprintf("%d", s.port), "--daemonize", "no") + args = append(args, "--port", cli.Sprintf("%d", s.port), "--daemonize", "no") } return s.startProcess(ctx, "redis-server", args, nil) @@ -427,7 +428,7 @@ func (s *RedisService) Start(ctx context.Context) error { func (s *RedisService) Stop() error { // Try graceful shutdown via redis-cli - cmd := exec.Command("redis-cli", "-p", fmt.Sprintf("%d", s.port), "shutdown", "nosave") + cmd := exec.Command("redis-cli", "-p", cli.Sprintf("%d", s.port), "shutdown", "nosave") cmd.Run() // Ignore errors return s.stopProcess() diff --git a/pkg/php/ssl.go b/pkg/php/ssl.go index 14498ad0..c81e7627 100644 --- a/pkg/php/ssl.go +++ b/pkg/php/ssl.go @@ -1,10 +1,11 @@ package php import ( - "fmt" "os" "os/exec" "path/filepath" + + "github.com/host-uk/core/pkg/cli" ) const ( @@ -25,13 +26,13 @@ func GetSSLDir(opts SSLOptions) (string, error) { if dir == "" { home, err := os.UserHomeDir() if err != nil { - return "", fmt.Errorf("failed to get home directory: %w", err) + return "", cli.WrapVerb(err, "get", "home directory") } dir = filepath.Join(home, DefaultSSLDir) } if err := os.MkdirAll(dir, 0755); err != nil { - return "", fmt.Errorf("failed to create SSL directory: %w", err) + return "", cli.WrapVerb(err, "create", "SSL directory") } return dir, nil @@ -44,8 +45,8 @@ func CertPaths(domain string, opts SSLOptions) (certFile, keyFile string, err er return "", "", err } - certFile = filepath.Join(dir, fmt.Sprintf("%s.pem", domain)) - keyFile = filepath.Join(dir, fmt.Sprintf("%s-key.pem", domain)) + certFile = filepath.Join(dir, cli.Sprintf("%s.pem", domain)) + keyFile = filepath.Join(dir, cli.Sprintf("%s-key.pem", domain)) return certFile, keyFile, nil } @@ -74,7 +75,7 @@ func CertsExist(domain string, opts SSLOptions) bool { func SetupSSL(domain string, opts SSLOptions) error { // Check if mkcert is installed if _, err := exec.LookPath("mkcert"); err != nil { - return fmt.Errorf("mkcert is not installed. Install it with: brew install mkcert (macOS) or see https://github.com/FiloSottile/mkcert") + return cli.Err("mkcert is not installed. Install it with: brew install mkcert (macOS) or see https://github.com/FiloSottile/mkcert") } dir, err := GetSSLDir(opts) @@ -85,12 +86,12 @@ func SetupSSL(domain string, opts SSLOptions) error { // Install local CA (idempotent operation) installCmd := exec.Command("mkcert", "-install") if output, err := installCmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to install mkcert CA: %w\n%s", err, output) + return cli.Err("failed to install mkcert CA: %v\n%s", err, output) } // Generate certificates - certFile := filepath.Join(dir, fmt.Sprintf("%s.pem", domain)) - keyFile := filepath.Join(dir, fmt.Sprintf("%s-key.pem", domain)) + certFile := filepath.Join(dir, cli.Sprintf("%s.pem", domain)) + keyFile := filepath.Join(dir, cli.Sprintf("%s-key.pem", domain)) // mkcert generates cert and key with specific naming genCmd := exec.Command("mkcert", @@ -103,7 +104,7 @@ func SetupSSL(domain string, opts SSLOptions) error { ) if output, err := genCmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to generate certificates: %w\n%s", err, output) + return cli.Err("failed to generate certificates: %v\n%s", err, output) } return nil @@ -134,13 +135,13 @@ func IsMkcertInstalled() bool { // InstallMkcertCA installs the local CA for mkcert. func InstallMkcertCA() error { if !IsMkcertInstalled() { - return fmt.Errorf("mkcert is not installed") + return cli.Err("mkcert is not installed") } cmd := exec.Command("mkcert", "-install") output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("failed to install mkcert CA: %w\n%s", err, output) + return cli.Err("failed to install mkcert CA: %v\n%s", err, output) } return nil @@ -149,13 +150,13 @@ func InstallMkcertCA() error { // GetMkcertCARoot returns the path to the mkcert CA root directory. func GetMkcertCARoot() (string, error) { if !IsMkcertInstalled() { - return "", fmt.Errorf("mkcert is not installed") + return "", cli.Err("mkcert is not installed") } cmd := exec.Command("mkcert", "-CAROOT") output, err := cmd.Output() if err != nil { - return "", fmt.Errorf("failed to get mkcert CA root: %w", err) + return "", cli.WrapVerb(err, "get", "mkcert CA root") } return filepath.Clean(string(output)), nil diff --git a/pkg/php/testing.go b/pkg/php/testing.go index 6ea69584..cb5bd9c5 100644 --- a/pkg/php/testing.go +++ b/pkg/php/testing.go @@ -2,11 +2,12 @@ package php import ( "context" - "fmt" "io" "os" "os/exec" "path/filepath" + + "github.com/host-uk/core/pkg/cli" ) // TestOptions configures PHP test execution. @@ -58,7 +59,7 @@ func RunTests(ctx context.Context, opts TestOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return cli.WrapVerb(err, "get", "working directory") } opts.Dir = cwd }