refactor(plugin): rename plugin files and update command structure

This commit is contained in:
Snider 2026-01-31 11:39:19 +00:00
parent a93cc3540a
commit 778ce64e4b
71 changed files with 1130 additions and 1871 deletions

View file

@ -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 <command>` 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 <query>` | GitHub search for core-* repos |
| Install package | `core pkg install <name>` | Clone and register package |
| Update packages | `core pkg update` | Pull latest for all packages |
| Run VM | `core vm run <image>` | 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 <query>
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 <id>
# View logs
core vm logs <id>
core vm logs -f <id> # Follow
# Execute command in VM
core vm exec <id> ls -la
core vm exec <id> /bin/sh
# Manage templates
core vm templates # List templates
core vm templates show <name> # Show template content
core vm templates vars <name> # 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 <os/arch>]
└── 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 <query>
└── Install: core pkg install <name>
└── 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`

View file

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

View file

@ -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"
}
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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, ", "))
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()
},
}

View file

@ -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)
}

View file

@ -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))

View file

@ -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))
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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"),
}

View file

@ -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),

View file

@ -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
}

View file

@ -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"),

View file

@ -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 {

View file

@ -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 <repo-name>",
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),

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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))
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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"),

View file

@ -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}),
)

View file

@ -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
}

View file

@ -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
}

View file

@ -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")

View file

@ -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"),

View file

@ -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
},
}

View file

@ -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
}

View file

@ -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
}

View file

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

View file

@ -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("")
}

View file

@ -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,
)
}

View file

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

View file

@ -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{

View file

@ -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
},
}

View file

@ -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)
}
}

View file

@ -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))
}

View file

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

View file

@ -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"},
}

View file

@ -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)

View file

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

View file

@ -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)
}

View file

@ -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()

View file

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

View file

@ -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
}