refactor(cli): move commands from cmd/ to pkg/ with self-registration

Implements defence in depth through build variants - only compiled code
exists in the binary. Commands now self-register via cli.RegisterCommands()
in their init() functions, mirroring the i18n.RegisterLocales() pattern.

Structure changes:
- cmd/{ai,build,ci,dev,docs,doctor,go,php,pkg,sdk,setup,test,vm}/ → pkg/*/cmd_*.go
- cmd/core_dev.go, cmd/core_ci.go → cmd/variants/{full,ci,php,minimal}.go
- Added pkg/cli/commands.go with RegisterCommands API
- Updated pkg/cli/runtime.go to attach registered commands

Build variants:
- go build           → full (21MB, all 13 command groups)
- go build -tags ci  → ci (18MB, build/ci/sdk/doctor)
- go build -tags php → php (14MB, php/doctor)
- go build -tags minimal → minimal (11MB, doctor only)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 21:55:55 +00:00
parent f2f7e27e77
commit 9931593f9d
87 changed files with 537 additions and 408 deletions

View file

@ -22,6 +22,10 @@ import (
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/framework"
"github.com/spf13/cobra" "github.com/spf13/cobra"
// Build variants import commands via self-registration.
// See cmd/variants/ for available variants: full, ci, php, minimal.
_ "github.com/host-uk/core/cmd/variants"
) )
const ( const (
@ -29,21 +33,7 @@ const (
appVersion = "0.1.0" appVersion = "0.1.0"
) )
// Terminal styles using Tailwind colour palette (from shared package).
var (
// coreStyle is used for primary headings and the CLI name.
coreStyle = cli.RepoNameStyle
// linkStyle is used for URLs and clickable references.
linkStyle = cli.LinkStyle
)
// rootCmd is the base command for the CLI.
var rootCmd = &cobra.Command{
Use: appName,
Short: "CLI tool for development and production",
Version: appVersion,
}
// Execute initialises and runs the CLI application. // Execute initialises and runs the CLI application.
// Commands are registered based on build tags (see core_ci.go and core_dev.go). // Commands are registered based on build tags (see core_ci.go and core_dev.go).
@ -63,13 +53,12 @@ func Execute() error {
} }
defer cli.Shutdown() defer cli.Shutdown()
return rootCmd.Execute() // Add completion command to the CLI's root
cli.RootCmd().AddCommand(completionCmd)
return cli.Execute()
} }
func init() {
// Add shell completion command
rootCmd.AddCommand(completionCmd)
}
// completionCmd generates shell completion scripts. // completionCmd generates shell completion scripts.
var completionCmd = &cobra.Command{ var completionCmd = &cobra.Command{

View file

@ -1,63 +0,0 @@
//go:build !ci
// core_dev.go registers commands for the full development binary.
//
// Build with: go build (default)
//
// This is the default build variant with all development tools:
// - dev: Multi-repo git workflows (commit, push, pull, sync)
// - ai: AI agent task management
// - go: Go module and build tools
// - php: Laravel/Composer development tools
// - build: Cross-platform compilation
// - ci: Release publishing
// - sdk: API compatibility checks
// - pkg: Package management
// - vm: LinuxKit VM management
// - docs: Documentation generation
// - setup: Repository cloning and setup
// - doctor: Environment health checks
// - test: Test runner with coverage
package cmd
import (
"github.com/host-uk/core/cmd/ai"
"github.com/host-uk/core/cmd/build"
"github.com/host-uk/core/cmd/ci"
"github.com/host-uk/core/cmd/dev"
"github.com/host-uk/core/cmd/docs"
"github.com/host-uk/core/cmd/doctor"
gocmd "github.com/host-uk/core/cmd/go"
"github.com/host-uk/core/cmd/php"
"github.com/host-uk/core/cmd/pkg"
"github.com/host-uk/core/cmd/sdk"
"github.com/host-uk/core/cmd/setup"
testcmd "github.com/host-uk/core/cmd/test"
"github.com/host-uk/core/cmd/vm"
)
func init() {
// Multi-repo workflow
dev.AddCommands(rootCmd)
// AI agent tools
ai.AddCommands(rootCmd)
// Language tooling
gocmd.AddCommands(rootCmd)
php.AddCommands(rootCmd)
// Build and release
build.AddCommands(rootCmd)
ci.AddCommands(rootCmd)
sdk.AddCommands(rootCmd)
// Environment management
pkg.AddCommands(rootCmd)
vm.AddCommands(rootCmd)
docs.AddCommands(rootCmd)
setup.AddCommands(rootCmd)
doctor.AddCommands(rootCmd)
testcmd.AddCommands(rootCmd)
}

View file

@ -1,15 +0,0 @@
// Package sdk provides SDK validation and API compatibility commands.
//
// Commands:
// - diff: Check for breaking API changes between spec versions
// - validate: Validate OpenAPI spec syntax
//
// Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk
package sdk
import "github.com/spf13/cobra"
// AddCommands registers the 'sdk' command and all subcommands.
func AddCommands(root *cobra.Command) {
root.AddCommand(sdkCmd)
}

View file

@ -1,6 +1,6 @@
//go:build ci //go:build ci
// core_ci.go registers commands for the minimal CI/release binary. // ci.go imports packages for the minimal CI/release binary.
// //
// Build with: go build -tags ci // Build with: go build -tags ci
// //
@ -12,18 +12,12 @@
// //
// Use this build to reduce binary size and attack surface in production. // Use this build to reduce binary size and attack surface in production.
package cmd package variants
import ( import (
"github.com/host-uk/core/cmd/build" // Commands via self-registration
"github.com/host-uk/core/cmd/ci" _ "github.com/host-uk/core/pkg/build/buildcmd"
"github.com/host-uk/core/cmd/doctor" _ "github.com/host-uk/core/pkg/ci"
"github.com/host-uk/core/cmd/sdk" _ "github.com/host-uk/core/pkg/doctor"
_ "github.com/host-uk/core/pkg/sdk"
) )
func init() {
build.AddCommands(rootCmd)
ci.AddCommands(rootCmd)
sdk.AddCommands(rootCmd)
doctor.AddCommands(rootCmd)
}

39
cmd/variants/full.go Normal file
View file

@ -0,0 +1,39 @@
//go:build !ci && !php && !minimal
// full.go imports all packages for the full development binary.
//
// Build with: go build (default)
//
// This is the default build variant with all development tools:
// - dev: Multi-repo git workflows (commit, push, pull, sync)
// - ai: AI agent task management
// - go: Go module and build tools
// - php: Laravel/Composer development tools
// - build: Cross-platform compilation
// - ci: Release publishing
// - sdk: API compatibility checks
// - pkg: Package management
// - vm: LinuxKit VM management
// - docs: Documentation generation
// - setup: Repository cloning and setup
// - doctor: Environment health checks
// - test: Test runner with coverage
package variants
import (
// Commands via self-registration
_ "github.com/host-uk/core/pkg/ai"
_ "github.com/host-uk/core/pkg/build/buildcmd"
_ "github.com/host-uk/core/pkg/ci"
_ "github.com/host-uk/core/pkg/dev"
_ "github.com/host-uk/core/pkg/docs"
_ "github.com/host-uk/core/pkg/doctor"
_ "github.com/host-uk/core/pkg/go"
_ "github.com/host-uk/core/pkg/php"
_ "github.com/host-uk/core/pkg/pkgcmd"
_ "github.com/host-uk/core/pkg/sdk"
_ "github.com/host-uk/core/pkg/setup"
_ "github.com/host-uk/core/pkg/test"
_ "github.com/host-uk/core/pkg/vm"
)

17
cmd/variants/minimal.go Normal file
View file

@ -0,0 +1,17 @@
//go:build minimal
// minimal.go imports only core packages for a minimal binary.
//
// Build with: go build -tags minimal
//
// This variant includes only the absolute essentials:
// - doctor: Environment verification
//
// Use this for the smallest possible binary with just health checks.
package variants
import (
// Commands via self-registration
_ "github.com/host-uk/core/pkg/doctor"
)

19
cmd/variants/php.go Normal file
View file

@ -0,0 +1,19 @@
//go:build php
// php.go imports packages for the PHP-only binary.
//
// Build with: go build -tags php
//
// This variant includes only PHP/Laravel development tools:
// - php: Laravel/Composer development tools
// - doctor: Environment verification
//
// Use this for PHP-focused workflows without other tooling.
package variants
import (
// Commands via self-registration
_ "github.com/host-uk/core/pkg/doctor"
_ "github.com/host-uk/core/pkg/php"
)

View file

@ -1,4 +1,4 @@
// ai.go defines styles and the AddAgenticCommands function for AI task management. // cmd_ai.go defines styles and the AddAgenticCommands function for AI task management.
package ai package ai

View file

@ -11,10 +11,15 @@
package ai package ai
import ( import (
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddAICommands)
}
var aiCmd = &cobra.Command{ var aiCmd = &cobra.Command{
Use: "ai", Use: "ai",
Short: i18n.T("cmd.ai.short"), Short: i18n.T("cmd.ai.short"),
@ -43,7 +48,7 @@ var claudeConfigCmd = &cobra.Command{
}, },
} }
func init() { func initCommands() {
// Add Claude subcommands // Add Claude subcommands
claudeCmd.AddCommand(claudeRunCmd) claudeCmd.AddCommand(claudeRunCmd)
claudeCmd.AddCommand(claudeConfigCmd) claudeCmd.AddCommand(claudeConfigCmd)
@ -55,8 +60,9 @@ func init() {
AddAgenticCommands(aiCmd) AddAgenticCommands(aiCmd)
} }
// AddCommands registers the 'ai' command and all subcommands. // AddAICommands registers the 'ai' command and all subcommands.
func AddCommands(root *cobra.Command) { func AddAICommands(root *cobra.Command) {
initCommands()
root.AddCommand(aiCmd) root.AddCommand(aiCmd)
} }

View file

@ -1,4 +1,4 @@
// ai_git.go implements git integration commands for task commits and PRs. // cmd_git.go implements git integration commands for task commits and PRs.
package ai package ai
@ -40,7 +40,7 @@ var taskCommitCmd = &cobra.Command{
taskID := args[0] taskID := args[0]
if taskCommitMessage == "" { if taskCommitMessage == "" {
return fmt.Errorf(i18n.T("cmd.ai.task_commit.message_required")) return fmt.Errorf("%s", i18n.T("cmd.ai.task_commit.message_required"))
} }
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
@ -143,7 +143,7 @@ var taskPRCmd = &cobra.Command{
} }
if branch == "main" || branch == "master" { if branch == "main" || branch == "master" {
return fmt.Errorf(i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch})) return fmt.Errorf("%s", i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch}))
} }
// Push current branch // Push current branch
@ -180,7 +180,7 @@ var taskPRCmd = &cobra.Command{
}, },
} }
func init() { func initGitFlags() {
// task:commit command flags // task:commit command flags
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message")) taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message"))
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope")) taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope"))
@ -194,6 +194,7 @@ func init() {
} }
func addTaskCommitCommand(parent *cobra.Command) { func addTaskCommitCommand(parent *cobra.Command) {
initGitFlags()
parent.AddCommand(taskCommitCmd) parent.AddCommand(taskCommitCmd)
} }

View file

@ -1,4 +1,4 @@
// ai_tasks.go implements task listing and viewing commands. // cmd_tasks.go implements task listing and viewing commands.
package ai package ai
@ -135,7 +135,7 @@ var taskCmd = &cobra.Command{
taskClaim = true // Auto-select implies claiming taskClaim = true // Auto-select implies claiming
} else { } else {
if taskID == "" { if taskID == "" {
return fmt.Errorf(i18n.T("cmd.ai.task.id_required")) return fmt.Errorf("%s", i18n.T("cmd.ai.task.id_required"))
} }
task, err = client.GetTask(ctx, taskID) task, err = client.GetTask(ctx, taskID)
@ -174,7 +174,7 @@ var taskCmd = &cobra.Command{
}, },
} }
func init() { func initTasksFlags() {
// tasks command flags // tasks command flags
tasksCmd.Flags().StringVar(&tasksStatus, "status", "", i18n.T("cmd.ai.tasks.flag.status")) tasksCmd.Flags().StringVar(&tasksStatus, "status", "", i18n.T("cmd.ai.tasks.flag.status"))
tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", i18n.T("cmd.ai.tasks.flag.priority")) tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", i18n.T("cmd.ai.tasks.flag.priority"))
@ -189,6 +189,7 @@ func init() {
} }
func addTasksCommand(parent *cobra.Command) { func addTasksCommand(parent *cobra.Command) {
initTasksFlags()
parent.AddCommand(tasksCmd) parent.AddCommand(tasksCmd)
} }

View file

@ -1,4 +1,4 @@
// ai_updates.go implements task update and completion commands. // cmd_updates.go implements task update and completion commands.
package ai package ai
@ -35,7 +35,7 @@ var taskUpdateCmd = &cobra.Command{
taskID := args[0] taskID := args[0]
if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
return fmt.Errorf(i18n.T("cmd.ai.task_update.flag_required")) return fmt.Errorf("%s", i18n.T("cmd.ai.task_update.flag_required"))
} }
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
@ -102,7 +102,7 @@ var taskCompleteCmd = &cobra.Command{
}, },
} }
func init() { func initUpdatesFlags() {
// task:update command flags // task:update command flags
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status")) taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status"))
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress")) taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress"))
@ -115,6 +115,7 @@ func init() {
} }
func addTaskUpdateCommand(parent *cobra.Command) { func addTaskUpdateCommand(parent *cobra.Command) {
initUpdatesFlags()
parent.AddCommand(taskUpdateCmd) parent.AddCommand(taskUpdateCmd)
} }

View file

@ -1,5 +1,5 @@
// Package build provides project build commands with auto-detection. // Package buildcmd provides project build commands with auto-detection.
package build package buildcmd
import ( import (
"embed" "embed"
@ -9,6 +9,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddBuildCommands)
}
// Style aliases from shared package // Style aliases from shared package
var ( var (
buildHeaderStyle = cli.TitleStyle buildHeaderStyle = cli.TitleStyle
@ -93,7 +97,7 @@ var sdkBuildCmd = &cobra.Command{
}, },
} }
func init() { func initBuildFlags() {
// Main build command flags // Main build command flags
buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.build.flag.type")) buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.build.flag.type"))
buildCmd.Flags().BoolVar(&ciMode, "ci", false, i18n.T("cmd.build.flag.ci")) buildCmd.Flags().BoolVar(&ciMode, "ci", false, i18n.T("cmd.build.flag.ci"))
@ -130,7 +134,8 @@ func init() {
buildCmd.AddCommand(sdkBuildCmd) buildCmd.AddCommand(sdkBuildCmd)
} }
// AddBuildCommand adds the new build command and its subcommands to the cobra app. // AddBuildCommands registers the 'build' command and all subcommands.
func AddBuildCommand(root *cobra.Command) { func AddBuildCommands(root *cobra.Command) {
initBuildFlags()
root.AddCommand(buildCmd) root.AddCommand(buildCmd)
} }

View file

@ -1,4 +1,4 @@
// Package build provides project build commands with auto-detection. // Package buildcmd provides project build commands with auto-detection.
// //
// Supports building: // Supports building:
// - Go projects (standard and cross-compilation) // - Go projects (standard and cross-compilation)
@ -14,11 +14,8 @@
// - build from-path: Build from a local static web app directory // - build from-path: Build from a local static web app directory
// - build pwa: Build from a live PWA URL // - build pwa: Build from a live PWA URL
// - build sdk: Generate API SDKs from OpenAPI spec // - build sdk: Generate API SDKs from OpenAPI spec
package build package buildcmd
import "github.com/spf13/cobra" // Note: The AddBuildCommands function is defined in cmd_build.go
// This file exists for documentation purposes and maintains the original
// AddCommands registers the 'build' command and all subcommands. // package documentation from commands.go.
func AddCommands(root *cobra.Command) {
AddBuildCommand(root)
}

View file

@ -1,9 +1,9 @@
// build_project.go implements the main project build logic. // cmd_project.go implements the main project build logic.
// //
// This handles auto-detection of project types (Go, Wails, Docker, LinuxKit, Taskfile) // This handles auto-detection of project types (Go, Wails, Docker, LinuxKit, Taskfile)
// and orchestrates the build process including signing, archiving, and checksums. // and orchestrates the build process including signing, archiving, and checksums.
package build package buildcmd
import ( import (
"context" "context"
@ -14,7 +14,7 @@ import (
"runtime" "runtime"
"strings" "strings"
buildpkg "github.com/host-uk/core/pkg/build" "github.com/host-uk/core/pkg/build"
"github.com/host-uk/core/pkg/build/builders" "github.com/host-uk/core/pkg/build/builders"
"github.com/host-uk/core/pkg/build/signing" "github.com/host-uk/core/pkg/build/signing"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
@ -29,17 +29,17 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// Load configuration from .core/build.yaml (or defaults) // Load configuration from .core/build.yaml (or defaults)
buildCfg, err := buildpkg.LoadConfig(projectDir) buildCfg, err := build.LoadConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load config"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load config"}), err)
} }
// Detect project type if not specified // Detect project type if not specified
var projectType buildpkg.ProjectType var projectType build.ProjectType
if buildType != "" { if buildType != "" {
projectType = buildpkg.ProjectType(buildType) projectType = build.ProjectType(buildType)
} else { } else {
projectType, err = buildpkg.PrimaryType(projectDir) projectType, err = build.PrimaryType(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project type"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project type"}), err)
} }
@ -49,7 +49,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// Determine targets // Determine targets
var buildTargets []buildpkg.Target var buildTargets []build.Target
if targetsFlag != "" { if targetsFlag != "" {
// Parse from command line // Parse from command line
buildTargets, err = parseTargets(targetsFlag) buildTargets, err = parseTargets(targetsFlag)
@ -61,7 +61,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
buildTargets = buildCfg.ToTargets() buildTargets = buildCfg.ToTargets()
} else { } else {
// Fall back to current OS/arch // Fall back to current OS/arch
buildTargets = []buildpkg.Target{ buildTargets = []build.Target{
{OS: runtime.GOOS, Arch: runtime.GOARCH}, {OS: runtime.GOOS, Arch: runtime.GOARCH},
} }
} }
@ -97,7 +97,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// Create build config for the builder // Create build config for the builder
cfg := &buildpkg.Config{ cfg := &build.Config{
ProjectDir: projectDir, ProjectDir: projectDir,
OutputDir: outputDir, OutputDir: outputDir,
Name: binaryName, Name: binaryName,
@ -156,7 +156,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
} }
// Convert buildpkg.Artifact to signing.Artifact // Convert build.Artifact to signing.Artifact
signingArtifacts := make([]signing.Artifact, len(artifacts)) signingArtifacts := make([]signing.Artifact, len(artifacts))
for i, a := range artifacts { for i, a := range artifacts {
signingArtifacts[i] = signing.Artifact{Path: a.Path, OS: a.OS, Arch: a.Arch} signingArtifacts[i] = signing.Artifact{Path: a.Path, OS: a.OS, Arch: a.Arch}
@ -180,14 +180,14 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// Archive artifacts if enabled // Archive artifacts if enabled
var archivedArtifacts []buildpkg.Artifact var archivedArtifacts []build.Artifact
if doArchive && len(artifacts) > 0 { if doArchive && len(artifacts) > 0 {
if !ciMode { if !ciMode {
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
} }
archivedArtifacts, err = buildpkg.ArchiveAll(artifacts) archivedArtifacts, err = build.ArchiveAll(artifacts)
if err != nil { if err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
@ -211,7 +211,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// Compute checksums if enabled // Compute checksums if enabled
var checksummedArtifacts []buildpkg.Artifact var checksummedArtifacts []build.Artifact
if doChecksum && len(archivedArtifacts) > 0 { if doChecksum && len(archivedArtifacts) > 0 {
checksummedArtifacts, err = computeAndWriteChecksums(ctx, projectDir, outputDir, archivedArtifacts, signCfg, ciMode) checksummedArtifacts, err = computeAndWriteChecksums(ctx, projectDir, outputDir, archivedArtifacts, signCfg, ciMode)
if err != nil { if err != nil {
@ -228,7 +228,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// Output results for CI mode // Output results for CI mode
if ciMode { if ciMode {
// Determine which artifacts to output (prefer checksummed > archived > raw) // Determine which artifacts to output (prefer checksummed > archived > raw)
var outputArtifacts []buildpkg.Artifact var outputArtifacts []build.Artifact
if len(checksummedArtifacts) > 0 { if len(checksummedArtifacts) > 0 {
outputArtifacts = checksummedArtifacts outputArtifacts = checksummedArtifacts
} else if len(archivedArtifacts) > 0 { } else if len(archivedArtifacts) > 0 {
@ -249,13 +249,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} }
// computeAndWriteChecksums computes checksums for artifacts and writes CHECKSUMS.txt. // computeAndWriteChecksums computes checksums for artifacts and writes CHECKSUMS.txt.
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) { func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []build.Artifact, signCfg signing.SignConfig, ciMode bool) ([]build.Artifact, error) {
if !ciMode { if !ciMode {
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
} }
checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts) checksummedArtifacts, err := build.ChecksumAll(artifacts)
if err != nil { if err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
@ -265,7 +265,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
// Write CHECKSUMS.txt // Write CHECKSUMS.txt
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt") checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil { if err := build.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err)
} }
@ -309,9 +309,9 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
} }
// parseTargets parses a comma-separated list of OS/arch pairs. // parseTargets parses a comma-separated list of OS/arch pairs.
func parseTargets(targetsFlag string) ([]buildpkg.Target, error) { func parseTargets(targetsFlag string) ([]build.Target, error) {
parts := strings.Split(targetsFlag, ",") parts := strings.Split(targetsFlag, ",")
var targets []buildpkg.Target var targets []build.Target
for _, part := range parts { for _, part := range parts {
part = strings.TrimSpace(part) part = strings.TrimSpace(part)
@ -324,7 +324,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.invalid_target", map[string]interface{}{"Target": part})) return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.invalid_target", map[string]interface{}{"Target": part}))
} }
targets = append(targets, buildpkg.Target{ targets = append(targets, build.Target{
OS: strings.TrimSpace(osArch[0]), OS: strings.TrimSpace(osArch[0]),
Arch: strings.TrimSpace(osArch[1]), Arch: strings.TrimSpace(osArch[1]),
}) })
@ -338,7 +338,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
} }
// formatTargets returns a human-readable string of targets. // formatTargets returns a human-readable string of targets.
func formatTargets(targets []buildpkg.Target) string { func formatTargets(targets []build.Target) string {
var parts []string var parts []string
for _, t := range targets { for _, t := range targets {
parts = append(parts, t.String()) parts = append(parts, t.String())
@ -347,21 +347,21 @@ func formatTargets(targets []buildpkg.Target) string {
} }
// getBuilder returns the appropriate builder for the project type. // getBuilder returns the appropriate builder for the project type.
func getBuilder(projectType buildpkg.ProjectType) (buildpkg.Builder, error) { func getBuilder(projectType build.ProjectType) (build.Builder, error) {
switch projectType { switch projectType {
case buildpkg.ProjectTypeWails: case build.ProjectTypeWails:
return builders.NewWailsBuilder(), nil return builders.NewWailsBuilder(), nil
case buildpkg.ProjectTypeGo: case build.ProjectTypeGo:
return builders.NewGoBuilder(), nil return builders.NewGoBuilder(), nil
case buildpkg.ProjectTypeDocker: case build.ProjectTypeDocker:
return builders.NewDockerBuilder(), nil return builders.NewDockerBuilder(), nil
case buildpkg.ProjectTypeLinuxKit: case build.ProjectTypeLinuxKit:
return builders.NewLinuxKitBuilder(), nil return builders.NewLinuxKitBuilder(), nil
case buildpkg.ProjectTypeTaskfile: case build.ProjectTypeTaskfile:
return builders.NewTaskfileBuilder(), nil return builders.NewTaskfileBuilder(), nil
case buildpkg.ProjectTypeNode: case build.ProjectTypeNode:
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.node_not_implemented")) return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.node_not_implemented"))
case buildpkg.ProjectTypePHP: case build.ProjectTypePHP:
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented")) return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented"))
default: default:
return nil, fmt.Errorf("%s: %s", i18n.T("cmd.build.error.unsupported_type"), projectType) return nil, fmt.Errorf("%s: %s", i18n.T("cmd.build.error.unsupported_type"), projectType)

View file

@ -1,10 +1,10 @@
// build_pwa.go implements PWA and legacy GUI build functionality. // cmd_pwa.go implements PWA and legacy GUI build functionality.
// //
// Supports building desktop applications from: // Supports building desktop applications from:
// - Local static web application directories // - Local static web application directories
// - Live PWA URLs (downloads and packages) // - Live PWA URLs (downloads and packages)
package build package buildcmd
import ( import (
"encoding/json" "encoding/json"

View file

@ -1,9 +1,9 @@
// build_sdk.go implements SDK generation from OpenAPI specifications. // cmd_sdk.go implements SDK generation from OpenAPI specifications.
// //
// Generates typed API clients for TypeScript, Python, Go, and PHP // Generates typed API clients for TypeScript, Python, Go, and PHP
// from OpenAPI/Swagger specifications. // from OpenAPI/Swagger specifications.
package build package buildcmd
import ( import (
"context" "context"

View file

@ -9,9 +9,16 @@
// Configuration via .core/release.yaml. // Configuration via .core/release.yaml.
package ci package ci
import "github.com/spf13/cobra" import (
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// AddCommands registers the 'ci' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddCICommands)
}
// AddCICommands registers the 'ci' command and all subcommands.
func AddCICommands(root *cobra.Command) {
root.AddCommand(ciCmd) root.AddCommand(ciCmd)
} }

View file

@ -2,6 +2,7 @@ package ci
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
@ -54,7 +55,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
// Check for publishers // Check for publishers
if len(cfg.Publishers) == 0 { if len(cfg.Publishers) == 0 {
return fmt.Errorf(i18n.T("cmd.ci.error.no_publishers")) return errors.New(i18n.T("cmd.ci.error.no_publishers"))
} }
// Publish pre-built artifacts // Publish pre-built artifacts

50
pkg/cli/commands.go Normal file
View file

@ -0,0 +1,50 @@
// Package cli provides the CLI runtime and utilities.
package cli
import (
"sync"
"github.com/spf13/cobra"
)
// CommandRegistration is a function that adds commands to the root.
type CommandRegistration func(root *cobra.Command)
var (
registeredCommands []CommandRegistration
registeredCommandsMu sync.Mutex
commandsAttached bool
)
// RegisterCommands registers a function that adds commands to the CLI.
// Call this in your package's init() to register commands.
//
// func init() {
// cli.RegisterCommands(AddCommands)
// }
//
// func AddCommands(root *cobra.Command) {
// root.AddCommand(myCmd)
// }
func RegisterCommands(fn CommandRegistration) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
registeredCommands = append(registeredCommands, fn)
// If commands already attached (CLI already running), attach immediately
if commandsAttached && instance != nil && instance.root != nil {
fn(instance.root)
}
}
// attachRegisteredCommands calls all registered command functions.
// Called by Init() after creating the root command.
func attachRegisteredCommands(root *cobra.Command) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
for _, fn := range registeredCommands {
fn(root)
}
commandsAttached = true
}

View file

@ -22,6 +22,7 @@ import (
"syscall" "syscall"
"github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/framework"
"github.com/spf13/cobra"
) )
var ( var (
@ -32,6 +33,7 @@ var (
// runtime is the CLI's internal Core runtime. // runtime is the CLI's internal Core runtime.
type runtime struct { type runtime struct {
core *framework.Core core *framework.Core
root *cobra.Command
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
@ -54,14 +56,24 @@ func Init(opts Options) error {
once.Do(func() { once.Do(func() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Create root command
rootCmd := &cobra.Command{
Use: opts.AppName,
Version: opts.Version,
}
// Attach all registered commands
attachRegisteredCommands(rootCmd)
// Build signal service options // Build signal service options
var signalOpts []SignalOption var signalOpts []SignalOption
if opts.OnReload != nil { if opts.OnReload != nil {
signalOpts = append(signalOpts, WithReloadHandler(opts.OnReload)) signalOpts = append(signalOpts, WithReloadHandler(opts.OnReload))
} }
// Build options: signal service + any additional services // Build options: app, signal service + any additional services
coreOpts := []framework.Option{ coreOpts := []framework.Option{
framework.WithApp(rootCmd),
framework.WithName("signal", newSignalService(cancel, signalOpts...)), framework.WithName("signal", newSignalService(cancel, signalOpts...)),
} }
coreOpts = append(coreOpts, opts.Services...) coreOpts = append(coreOpts, opts.Services...)
@ -76,6 +88,7 @@ func Init(opts Options) error {
instance = &runtime{ instance = &runtime{
core: c, core: c,
root: rootCmd,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
} }
@ -102,6 +115,19 @@ func Core() *framework.Core {
return instance.core return instance.core
} }
// RootCmd returns the CLI's root cobra command.
func RootCmd() *cobra.Command {
mustInit()
return instance.root
}
// Execute runs the CLI root command.
// Returns an error if the command fails.
func Execute() error {
mustInit()
return instance.root.Execute()
}
// Context returns the CLI's root context. // Context returns the CLI's root context.
// Cancelled on SIGINT/SIGTERM. // Cancelled on SIGINT/SIGTERM.
func Context() context.Context { func Context() context.Context {

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
devpkg "github.com/host-uk/core/pkg/dev"
"github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/framework"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
) )
@ -24,7 +23,7 @@ type WorkBundleOptions struct {
// Includes: dev (orchestration), git, agentic services. // Includes: dev (orchestration), git, agentic services.
func NewWorkBundle(opts WorkBundleOptions) (*WorkBundle, error) { func NewWorkBundle(opts WorkBundleOptions) (*WorkBundle, error) {
c, err := framework.New( c, err := framework.New(
framework.WithService(devpkg.NewService(devpkg.ServiceOptions{ framework.WithService(NewService(ServiceOptions{
RegistryPath: opts.RegistryPath, RegistryPath: opts.RegistryPath,
})), })),
framework.WithService(git.NewService(git.ServiceOptions{})), framework.WithService(git.NewService(git.ServiceOptions{})),
@ -64,7 +63,7 @@ type StatusBundleOptions struct {
// Includes: dev (orchestration), git services. No agentic - commits not available. // Includes: dev (orchestration), git services. No agentic - commits not available.
func NewStatusBundle(opts StatusBundleOptions) (*StatusBundle, error) { func NewStatusBundle(opts StatusBundleOptions) (*StatusBundle, error) {
c, err := framework.New( c, err := framework.New(
framework.WithService(devpkg.NewService(devpkg.ServiceOptions{ framework.WithService(NewService(ServiceOptions{
RegistryPath: opts.RegistryPath, RegistryPath: opts.RegistryPath,
})), })),
framework.WithService(git.NewService(git.ServiceOptions{})), framework.WithService(git.NewService(git.ServiceOptions{})),

View file

@ -2,6 +2,7 @@ package dev
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -68,7 +69,7 @@ func addCICommand(parent *cobra.Command) {
func runCI(registryPath string, branch string, failedOnly bool) error { func runCI(registryPath string, branch string, failedOnly bool) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf(i18n.T("error.gh_not_found")) return errors.New(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry // Find or use provided registry

View file

@ -34,6 +34,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddDevCommands)
}
// Style aliases from shared package // Style aliases from shared package
var ( var (
successStyle = cli.SuccessStyle successStyle = cli.SuccessStyle
@ -52,8 +56,8 @@ var (
cleanStyle = cli.GitCleanStyle.Padding(0, 1) cleanStyle = cli.GitCleanStyle.Padding(0, 1)
) )
// AddCommands registers the 'dev' command and all subcommands. // AddDevCommands registers the 'dev' command and all subcommands.
func AddCommands(root *cobra.Command) { func AddDevCommands(root *cobra.Command) {
devCmd := &cobra.Command{ devCmd := &cobra.Command{
Use: "dev", Use: "dev",
Short: i18n.T("cmd.dev.short"), Short: i18n.T("cmd.dev.short"),

View file

@ -1,6 +1,7 @@
package dev package dev
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
@ -55,14 +56,14 @@ func runImpact(registryPath string, repoName string) error {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
} else { } else {
return fmt.Errorf(i18n.T("cmd.dev.impact.requires_registry")) return errors.New(i18n.T("cmd.dev.impact.requires_registry"))
} }
} }
// Check repo exists // Check repo exists
repo, exists := reg.Get(repoName) repo, exists := reg.Get(repoName)
if !exists { if !exists {
return fmt.Errorf(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName})) return errors.New(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName}))
} }
// Build reverse dependency graph // Build reverse dependency graph

View file

@ -2,6 +2,7 @@ package dev
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -82,7 +83,7 @@ func addIssuesCommand(parent *cobra.Command) {
func runIssues(registryPath string, limit int, assignee string) error { func runIssues(registryPath string, limit int, assignee string) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf(i18n.T("error.gh_not_found")) return errors.New(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry, fall back to directory scan // Find or use provided registry, fall back to directory scan

View file

@ -2,6 +2,7 @@ package dev
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -79,7 +80,7 @@ func addReviewsCommand(parent *cobra.Command) {
func runReviews(registryPath string, author string, showAll bool) error { func runReviews(registryPath string, author string, showAll bool) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf(i18n.T("error.gh_not_found")) return errors.New(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry, fall back to directory scan // Find or use provided registry, fall back to directory scan

View file

@ -2,6 +2,7 @@ package dev
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -118,7 +119,7 @@ func runVMBoot(memory, cpus int, fresh bool) error {
} }
if !d.IsInstalled() { if !d.IsInstalled() {
return fmt.Errorf(i18n.T("cmd.dev.vm.not_installed")) return errors.New(i18n.T("cmd.dev.vm.not_installed"))
} }
opts := devops.DefaultBootOptions() opts := devops.DefaultBootOptions()

View file

@ -8,8 +8,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"

View file

@ -8,9 +8,16 @@
// to a central location for unified documentation builds. // to a central location for unified documentation builds.
package docs package docs
import "github.com/spf13/cobra" import (
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// AddCommands registers the 'docs' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddDocsCommands)
}
// AddDocsCommands registers the 'docs' command and all subcommands.
func AddDocsCommands(root *cobra.Command) {
root.AddCommand(docsCmd) root.AddCommand(docsCmd)
} }

View file

@ -10,9 +10,16 @@
// Provides platform-specific installation instructions for missing tools. // Provides platform-specific installation instructions for missing tools.
package doctor package doctor
import "github.com/spf13/cobra" import (
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// AddCommands registers the 'doctor' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddDoctorCommands)
}
// AddDoctorCommands registers the 'doctor' command and all subcommands.
func AddDoctorCommands(root *cobra.Command) {
root.AddCommand(doctorCmd) root.AddCommand(doctorCmd)
} }

View file

@ -14,9 +14,8 @@
// Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS. // Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS.
package gocmd package gocmd
import "github.com/spf13/cobra" import "github.com/host-uk/core/pkg/cli"
// AddCommands registers the 'go' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddGoCommands)
AddGoCommands(root)
} }

View file

@ -1,6 +1,7 @@
package gocmd package gocmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -179,7 +180,7 @@ func addGoCovCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "discover test packages"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "discover test packages"}), err)
} }
if len(pkgs) == 0 { if len(pkgs) == 0 {
return fmt.Errorf(i18n.T("cmd.go.cov.error.no_packages")) return errors.New(i18n.T("cmd.go.cov.error.no_packages"))
} }
pkg = strings.Join(pkgs, " ") pkg = strings.Join(pkgs, " ")
} }
@ -275,7 +276,7 @@ func addGoCovCommand(parent *cobra.Command) {
"Actual": fmt.Sprintf("%.1f", totalCov), "Actual": fmt.Sprintf("%.1f", totalCov),
"Threshold": fmt.Sprintf("%.1f", covThreshold), "Threshold": fmt.Sprintf("%.1f", covThreshold),
}))) })))
return fmt.Errorf(i18n.T("cmd.go.cov.error.below_threshold")) return errors.New(i18n.T("cmd.go.cov.error.below_threshold"))
} }
if testErr != nil { if testErr != nil {

View file

@ -1,6 +1,7 @@
package gocmd package gocmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -193,7 +194,7 @@ func addGoWorkCommand(parent *cobra.Command) {
// Auto-detect modules // Auto-detect modules
modules := findGoModules(".") modules := findGoModules(".")
if len(modules) == 0 { if len(modules) == 0 {
return fmt.Errorf(i18n.T("cmd.go.work.error.no_modules")) return errors.New(i18n.T("cmd.go.work.error.no_modules"))
} }
for _, mod := range modules { for _, mod := range modules {
execCmd := exec.Command("go", "work", "use", mod) execCmd := exec.Command("go", "work", "use", mod)

View file

@ -8,6 +8,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddPHPCommands)
}
// Style aliases from shared // Style aliases from shared
var ( var (
successStyle = cli.SuccessStyle successStyle = cli.SuccessStyle

View file

@ -2,12 +2,12 @@ package php
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -83,14 +83,14 @@ type linuxKitBuildOptions struct {
} }
func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error { func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error {
if !phppkg.IsPHPProject(projectDir) { if !IsPHPProject(projectDir) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) 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")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker"))
// Show detected configuration // Show detected configuration
config, err := phppkg.DetectDockerfileConfig(projectDir) config, err := DetectDockerfileConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project configuration"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project configuration"}), err)
} }
@ -105,7 +105,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
fmt.Println() fmt.Println()
// Build options // Build options
buildOpts := phppkg.DockerBuildOptions{ buildOpts := DockerBuildOptions{
ProjectDir: projectDir, ProjectDir: projectDir,
ImageName: opts.ImageName, ImageName: opts.ImageName,
Tag: opts.Tag, Tag: opts.Tag,
@ -116,7 +116,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
} }
if buildOpts.ImageName == "" { if buildOpts.ImageName == "" {
buildOpts.ImageName = phppkg.GetLaravelAppName(projectDir) buildOpts.ImageName = GetLaravelAppName(projectDir)
if buildOpts.ImageName == "" { if buildOpts.ImageName == "" {
buildOpts.ImageName = "php-app" buildOpts.ImageName = "php-app"
} }
@ -134,7 +134,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
} }
fmt.Println() fmt.Println()
if err := phppkg.BuildDocker(ctx, buildOpts); err != nil { if err := BuildDocker(ctx, buildOpts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err)
} }
@ -147,13 +147,13 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
} }
func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error { func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error {
if !phppkg.IsPHPProject(projectDir) { if !IsPHPProject(projectDir) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) 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")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit"))
buildOpts := phppkg.LinuxKitBuildOptions{ buildOpts := LinuxKitBuildOptions{
ProjectDir: projectDir, ProjectDir: projectDir,
OutputPath: opts.OutputPath, OutputPath: opts.OutputPath,
Format: opts.Format, Format: opts.Format,
@ -172,7 +172,7 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format)
fmt.Println() fmt.Println()
if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil { if err := BuildLinuxKit(ctx, buildOpts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err)
} }
@ -201,19 +201,19 @@ func addPHPServeCommand(parent *cobra.Command) {
// Try to detect from current directory // Try to detect from current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err == nil { if err == nil {
imageName = phppkg.GetLaravelAppName(cwd) imageName = GetLaravelAppName(cwd)
if imageName != "" { if imageName != "" {
imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-")) imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-"))
} }
} }
if imageName == "" { if imageName == "" {
return fmt.Errorf(i18n.T("cmd.php.serve.name_required")) return errors.New(i18n.T("cmd.php.serve.name_required"))
} }
} }
ctx := context.Background() ctx := context.Background()
opts := phppkg.ServeOptions{ opts := ServeOptions{
ImageName: imageName, ImageName: imageName,
Tag: serveTag, Tag: serveTag,
ContainerName: serveContainerName, ContainerName: serveContainerName,
@ -245,7 +245,7 @@ func addPHPServeCommand(parent *cobra.Command) {
dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort)
fmt.Println() fmt.Println()
if err := phppkg.ServeProduction(ctx, opts); err != nil { if err := ServeProduction(ctx, opts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "start container"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "start container"}), err)
} }
@ -279,7 +279,7 @@ func addPHPShellCommand(parent *cobra.Command) {
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]})) 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]}))
if err := phppkg.Shell(ctx, args[0]); err != nil { if err := Shell(ctx, args[0]); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "open shell"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "open shell"}), err)
} }

View file

@ -8,7 +8,6 @@ import (
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -50,23 +49,23 @@ func addPHPDeployCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
env := phppkg.EnvProduction env := EnvProduction
if deployStaging { if deployStaging {
env = phppkg.EnvStaging 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})) 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}))
ctx := context.Background() ctx := context.Background()
opts := phppkg.DeployOptions{ opts := DeployOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
Force: deployForce, Force: deployForce,
Wait: deployWait, Wait: deployWait,
} }
status, err := phppkg.Deploy(ctx, opts) status, err := Deploy(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err)
} }
@ -74,7 +73,7 @@ func addPHPDeployCommand(parent *cobra.Command) {
printDeploymentStatus(status) printDeploymentStatus(status)
if deployWait { if deployWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"})) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"}))
} else { } else {
fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status})) fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status}))
@ -110,22 +109,22 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
env := phppkg.EnvProduction env := EnvProduction
if deployStatusStaging { if deployStatusStaging {
env = phppkg.EnvStaging env = EnvStaging
} }
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("common.progress.checking", map[string]any{"Item": "deployment status"})) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("common.progress.checking", map[string]any{"Item": "deployment status"}))
ctx := context.Background() ctx := context.Background()
opts := phppkg.StatusOptions{ opts := StatusOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
DeploymentID: deployStatusDeploymentID, DeploymentID: deployStatusDeploymentID,
} }
status, err := phppkg.DeployStatus(ctx, opts) status, err := DeployStatus(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get status"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get status"}), err)
} }
@ -159,23 +158,23 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
env := phppkg.EnvProduction env := EnvProduction
if rollbackStaging { if rollbackStaging {
env = phppkg.EnvStaging 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})) 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}))
ctx := context.Background() ctx := context.Background()
opts := phppkg.RollbackOptions{ opts := RollbackOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
DeploymentID: rollbackDeploymentID, DeploymentID: rollbackDeploymentID,
Wait: rollbackWait, Wait: rollbackWait,
} }
status, err := phppkg.Rollback(ctx, opts) status, err := Rollback(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err)
} }
@ -183,7 +182,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
printDeploymentStatus(status) printDeploymentStatus(status)
if rollbackWait { if rollbackWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"})) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"}))
} else { } else {
fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status})) fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status}))
@ -219,9 +218,9 @@ func addPHPDeployListCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
env := phppkg.EnvProduction env := EnvProduction
if deployListStaging { if deployListStaging {
env = phppkg.EnvStaging env = EnvStaging
} }
limit := deployListLimit limit := deployListLimit
@ -233,7 +232,7 @@ func addPHPDeployListCommand(parent *cobra.Command) {
ctx := context.Background() ctx := context.Background()
deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit) deployments, err := ListDeployments(ctx, cwd, env, limit)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list deployments"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list deployments"}), err)
} }
@ -257,7 +256,7 @@ func addPHPDeployListCommand(parent *cobra.Command) {
parent.AddCommand(listCmd) parent.AddCommand(listCmd)
} }
func printDeploymentStatus(status *phppkg.DeploymentStatus) { func printDeploymentStatus(status *DeploymentStatus) {
// Status with color // Status with color
statusStyle := phpDeployStyle statusStyle := phpDeployStyle
switch status.Status { switch status.Status {
@ -310,7 +309,7 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
} }
} }
func printDeploymentSummary(index int, status *phppkg.DeploymentStatus) { func printDeploymentSummary(index int, status *DeploymentStatus) {
// Status with color // Status with color
statusStyle := phpDeployStyle statusStyle := phpDeployStyle
switch status.Status { switch status.Status {

View file

@ -3,6 +3,7 @@ package php
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
@ -12,7 +13,6 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -72,12 +72,12 @@ func runPHPDev(opts phpDevOptions) error {
} }
// Check if this is a Laravel project // Check if this is a Laravel project
if !phppkg.IsLaravelProject(cwd) { if !IsLaravelProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel")) return errors.New(i18n.T("cmd.php.error.not_laravel"))
} }
// Get app name for display // Get app name for display
appName := phppkg.GetLaravelAppName(cwd) appName := GetLaravelAppName(cwd)
if appName == "" { if appName == "" {
appName = "Laravel" appName = "Laravel"
} }
@ -85,7 +85,7 @@ func runPHPDev(opts phpDevOptions) error {
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})) 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}))
// Detect services // Detect services
services := phppkg.DetectServices(cwd) services := DetectServices(cwd)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services"))
for _, svc := range services { for _, svc := range services {
fmt.Printf(" %s %s\n", successStyle.Render("*"), svc) fmt.Printf(" %s %s\n", successStyle.Render("*"), svc)
@ -98,7 +98,7 @@ func runPHPDev(opts phpDevOptions) error {
port = 8000 port = 8000
} }
devOpts := phppkg.Options{ devOpts := Options{
Dir: cwd, Dir: cwd,
NoVite: opts.NoVite, NoVite: opts.NoVite,
NoHorizon: opts.NoHorizon, NoHorizon: opts.NoHorizon,
@ -110,7 +110,7 @@ func runPHPDev(opts phpDevOptions) error {
} }
// Create and start dev server // Create and start dev server
server := phppkg.NewDevServer(devOpts) server := NewDevServer(devOpts)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -135,7 +135,7 @@ func runPHPDev(opts phpDevOptions) error {
fmt.Println() fmt.Println()
// Print URLs // Print URLs
appURL := phppkg.GetLaravelAppURL(cwd) appURL := GetLaravelAppURL(cwd)
if appURL == "" { if appURL == "" {
if opts.HTTPS { if opts.HTTPS {
appURL = fmt.Sprintf("https://localhost:%d", port) appURL = fmt.Sprintf("https://localhost:%d", port)
@ -146,7 +146,7 @@ func runPHPDev(opts phpDevOptions) error {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL))
// Check for Vite // Check for Vite
if !opts.NoVite && containsService(services, phppkg.ServiceVite) { if !opts.NoVite && containsService(services, ServiceVite) {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173"))
} }
@ -208,12 +208,12 @@ func runPHPLogs(service string, follow bool) error {
return err return err
} }
if !phppkg.IsLaravelProject(cwd) { if !IsLaravelProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short")) return errors.New(i18n.T("cmd.php.error.not_laravel_short"))
} }
// Create a minimal server just to access logs // Create a minimal server just to access logs
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd}) server := NewDevServer(Options{Dir: cwd})
logsReader, err := server.Logs(service, follow) logsReader, err := server.Logs(service, follow)
if err != nil { if err != nil {
@ -268,7 +268,7 @@ func runPHPStop() error {
// We need to find running processes // We need to find running processes
// This is a simplified version - in practice you'd want to track PIDs // This is a simplified version - in practice you'd want to track PIDs
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd}) server := NewDevServer(Options{Dir: cwd})
if err := server.Stop(); err != nil { if err := server.Stop(); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "stop services"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "stop services"}), err)
} }
@ -295,11 +295,11 @@ func runPHPStatus() error {
return err return err
} }
if !phppkg.IsLaravelProject(cwd) { if !IsLaravelProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short")) return errors.New(i18n.T("cmd.php.error.not_laravel_short"))
} }
appName := phppkg.GetLaravelAppName(cwd) appName := GetLaravelAppName(cwd)
if appName == "" { if appName == "" {
appName = "Laravel" appName = "Laravel"
} }
@ -307,7 +307,7 @@ func runPHPStatus() error {
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.project")), appName) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.project")), appName)
// Detect available services // Detect available services
services := phppkg.DetectServices(cwd) services := DetectServices(cwd)
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services"))) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services")))
for _, svc := range services { for _, svc := range services {
style := getServiceStyle(string(svc)) style := getServiceStyle(string(svc))
@ -316,19 +316,19 @@ func runPHPStatus() error {
fmt.Println() fmt.Println()
// Package manager // Package manager
pm := phppkg.DetectPackageManager(cwd) pm := DetectPackageManager(cwd)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm)
// FrankenPHP status // FrankenPHP status
if phppkg.IsFrankenPHPProject(cwd) { if IsFrankenPHPProject(cwd) {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP") fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP")
} }
// SSL status // SSL status
appURL := phppkg.GetLaravelAppURL(cwd) appURL := GetLaravelAppURL(cwd)
if appURL != "" { if appURL != "" {
domain := phppkg.ExtractDomainFromURL(appURL) domain := ExtractDomainFromURL(appURL)
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { 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"))) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed")))
} else { } 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"))) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup")))
@ -362,9 +362,9 @@ func runPHPSSL(domain string) error {
// Get domain from APP_URL if not specified // Get domain from APP_URL if not specified
if domain == "" { if domain == "" {
appURL := phppkg.GetLaravelAppURL(cwd) appURL := GetLaravelAppURL(cwd)
if appURL != "" { if appURL != "" {
domain = phppkg.ExtractDomainFromURL(appURL) domain = ExtractDomainFromURL(appURL)
} }
} }
if domain == "" { if domain == "" {
@ -372,32 +372,32 @@ func runPHPSSL(domain string) error {
} }
// Check if mkcert is installed // Check if mkcert is installed
if !phppkg.IsMkcertInstalled() { if !IsMkcertInstalled() {
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed")) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed"))
fmt.Printf("\n%s\n", i18n.T("common.hint.install_with")) 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_macos"))
fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux")) fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux"))
return fmt.Errorf(i18n.T("cmd.php.error.mkcert_not_installed")) 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})) fmt.Printf("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain}))
// Check if certs already exist // Check if certs already exist
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { if CertsExist(domain, SSLOptions{}) {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.skip")), i18n.T("cmd.php.ssl.certs_exist")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.skip")), i18n.T("cmd.php.ssl.certs_exist"))
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) 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.cert_label")), certFile)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
return nil return nil
} }
// Setup SSL // Setup SSL
if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil { if err := SetupSSL(domain, SSLOptions{}); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "setup SSL"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "setup SSL"}), err)
} }
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) certFile, keyFile, _ := CertPaths(domain, SSLOptions{})
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("cmd.php.ssl.certs_created")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.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.cert_label")), certFile)
@ -408,7 +408,7 @@ func runPHPSSL(domain string) error {
// Helper functions for dev commands // Helper functions for dev commands
func printServiceStatuses(statuses []phppkg.ServiceStatus) { func printServiceStatuses(statuses []ServiceStatus) {
for _, s := range statuses { for _, s := range statuses {
style := getServiceStyle(s.Name) style := getServiceStyle(s.Name)
var statusText string var statusText string
@ -488,7 +488,7 @@ func getServiceStyle(name string) lipgloss.Style {
} }
} }
func containsService(services []phppkg.DetectedService, target phppkg.DetectedService) bool { func containsService(services []DetectedService, target DetectedService) bool {
for _, s := range services { for _, s := range services {
if s == target { if s == target {
return true return true

View file

@ -5,7 +5,6 @@ import (
"os" "os"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -37,7 +36,7 @@ func addPHPPackagesLinkCommand(parent *cobra.Command) {
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking"))
if err := phppkg.LinkPackages(cwd, args); err != nil { if err := LinkPackages(cwd, args); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "link packages"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "link packages"}), err)
} }
@ -63,7 +62,7 @@ func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking"))
if err := phppkg.UnlinkPackages(cwd, args); err != nil { if err := UnlinkPackages(cwd, args); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "unlink packages"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "unlink packages"}), err)
} }
@ -88,7 +87,7 @@ func addPHPPackagesUpdateCommand(parent *cobra.Command) {
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating"))
if err := phppkg.UpdatePackages(cwd, args); err != nil { if err := UpdatePackages(cwd, args); err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err)
} }
@ -111,7 +110,7 @@ func addPHPPackagesListCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
packages, err := phppkg.ListLinkedPackages(cwd) packages, err := ListLinkedPackages(cwd)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list packages"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list packages"}), err)
} }

View file

@ -10,7 +10,6 @@ import (
"github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/framework"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/host-uk/core/pkg/process" "github.com/host-uk/core/pkg/process"
) )
@ -78,11 +77,11 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
} }
case "fmt": case "fmt":
formatter, found := phppkg.DetectFormatter(r.dir) formatter, found := DetectFormatter(r.dir)
if !found { if !found {
return nil return nil
} }
if formatter == phppkg.FormatterPint { if formatter == FormatterPint {
vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint") vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint")
cmd := "pint" cmd := "pint"
if _, err := os.Stat(vendorBin); err == nil { if _, err := os.Stat(vendorBin); err == nil {
@ -103,7 +102,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
return nil return nil
case "stan": case "stan":
_, found := phppkg.DetectAnalyser(r.dir) _, found := DetectAnalyser(r.dir)
if !found { if !found {
return nil return nil
} }
@ -121,7 +120,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
} }
case "psalm": case "psalm":
_, found := phppkg.DetectPsalm(r.dir) _, found := DetectPsalm(r.dir)
if !found { if !found {
return nil return nil
} }
@ -158,7 +157,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
// Tests depend on stan (or psalm if available) // Tests depend on stan (or psalm if available)
after := []string{"stan"} after := []string{"stan"}
if _, found := phppkg.DetectPsalm(r.dir); found { if _, found := DetectPsalm(r.dir); found {
after = []string{"psalm"} after = []string{"psalm"}
} }
@ -171,7 +170,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
} }
case "rector": case "rector":
if !phppkg.DetectRector(r.dir) { if !DetectRector(r.dir) {
return nil return nil
} }
vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector") vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector")
@ -193,7 +192,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
} }
case "infection": case "infection":
if !phppkg.DetectInfection(r.dir) { if !DetectInfection(r.dir) {
return nil return nil
} }
vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection") vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection")
@ -215,11 +214,11 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec {
} }
// Run executes all QA checks and returns the results. // Run executes all QA checks and returns the results.
func (r *QARunner) Run(ctx context.Context, stages []phppkg.QAStage) (*QARunResult, error) { func (r *QARunner) Run(ctx context.Context, stages []QAStage) (*QARunResult, error) {
// Collect all checks from all stages // Collect all checks from all stages
var allChecks []string var allChecks []string
for _, stage := range stages { for _, stage := range stages {
checks := phppkg.GetQAChecks(r.dir, stage) checks := GetQAChecks(r.dir, stage)
allChecks = append(allChecks, checks...) allChecks = append(allChecks, checks...)
} }

View file

@ -2,13 +2,13 @@ package php
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,15 +30,15 @@ func addPHPTestCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) 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("common.progress.running", map[string]any{"Task": "tests"})) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("common.progress.running", map[string]any{"Task": "tests"}))
ctx := context.Background() ctx := context.Background()
opts := phppkg.TestOptions{ opts := TestOptions{
Dir: cwd, Dir: cwd,
Filter: testFilter, Filter: testFilter,
Parallel: testParallel, Parallel: testParallel,
@ -50,7 +50,7 @@ func addPHPTestCommand(parent *cobra.Command) {
opts.Groups = []string{testGroup} opts.Groups = []string{testGroup}
} }
if err := phppkg.RunTests(ctx, opts); err != nil { if err := RunTests(ctx, opts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "run tests"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "run tests"}), err)
} }
@ -82,14 +82,14 @@ func addPHPFmtCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Detect formatter // Detect formatter
formatter, found := phppkg.DetectFormatter(cwd) formatter, found := DetectFormatter(cwd)
if !found { if !found {
return fmt.Errorf(i18n.T("cmd.php.fmt.no_formatter")) return errors.New(i18n.T("cmd.php.fmt.no_formatter"))
} }
var msg string var msg string
@ -102,7 +102,7 @@ func addPHPFmtCommand(parent *cobra.Command) {
ctx := context.Background() ctx := context.Background()
opts := phppkg.FormatOptions{ opts := FormatOptions{
Dir: cwd, Dir: cwd,
Fix: fmtFix, Fix: fmtFix,
Diff: fmtDiff, Diff: fmtDiff,
@ -114,7 +114,7 @@ func addPHPFmtCommand(parent *cobra.Command) {
opts.Paths = args opts.Paths = args
} }
if err := phppkg.Format(ctx, opts); err != nil { if err := Format(ctx, opts); err != nil {
if fmtFix { if fmtFix {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err)
} }
@ -153,21 +153,21 @@ func addPHPStanCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Detect analyser // Detect analyser
_, found := phppkg.DetectAnalyser(cwd) _, found := DetectAnalyser(cwd)
if !found { if !found {
return fmt.Errorf(i18n.T("cmd.php.analyse.no_analyser")) 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.T("common.progress.running", map[string]any{"Task": "static analysis"})) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("common.progress.running", map[string]any{"Task": "static analysis"}))
ctx := context.Background() ctx := context.Background()
opts := phppkg.AnalyseOptions{ opts := AnalyseOptions{
Dir: cwd, Dir: cwd,
Level: stanLevel, Level: stanLevel,
Memory: stanMemory, Memory: stanMemory,
@ -179,7 +179,7 @@ func addPHPStanCommand(parent *cobra.Command) {
opts.Paths = args opts.Paths = args
} }
if err := phppkg.Analyse(ctx, opts); err != nil { if err := Analyse(ctx, opts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err)
} }
@ -216,17 +216,17 @@ func addPHPPsalmCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Check if Psalm is available // Check if Psalm is available
_, found := phppkg.DetectPsalm(cwd) _, found := DetectPsalm(cwd)
if !found { if !found {
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.psalm.not_found")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.psalm.not_found"))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.psalm.install")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.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")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup"))
return fmt.Errorf(i18n.T("cmd.php.error.psalm_not_installed")) return errors.New(i18n.T("cmd.php.error.psalm_not_installed"))
} }
var msg string var msg string
@ -239,7 +239,7 @@ func addPHPPsalmCommand(parent *cobra.Command) {
ctx := context.Background() ctx := context.Background()
opts := phppkg.PsalmOptions{ opts := PsalmOptions{
Dir: cwd, Dir: cwd,
Level: psalmLevel, Level: psalmLevel,
Fix: psalmFix, Fix: psalmFix,
@ -248,7 +248,7 @@ func addPHPPsalmCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
} }
if err := phppkg.RunPsalm(ctx, opts); err != nil { if err := RunPsalm(ctx, opts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err)
} }
@ -281,15 +281,15 @@ func addPHPAuditCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) 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")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning"))
ctx := context.Background() ctx := context.Background()
results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{ results, err := RunAudit(ctx, AuditOptions{
Dir: cwd, Dir: cwd,
JSON: auditJSONOutput, JSON: auditJSONOutput,
Fix: auditFix, Fix: auditFix,
@ -338,11 +338,11 @@ func addPHPAuditCommand(parent *cobra.Command) {
if totalVulns > 0 { if totalVulns > 0 {
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns})) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns}))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.fix")), i18n.T("common.hint.fix_deps")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.fix")), i18n.T("common.hint.fix_deps"))
return fmt.Errorf(i18n.T("cmd.php.error.vulns_found")) return errors.New(i18n.T("cmd.php.error.vulns_found"))
} }
if hasErrors { if hasErrors {
return fmt.Errorf(i18n.T("cmd.php.audit.completed_errors")) return errors.New(i18n.T("cmd.php.audit.completed_errors"))
} }
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("cmd.php.audit.all_secure")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("cmd.php.audit.all_secure"))
@ -374,15 +374,15 @@ func addPHPSecurityCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) 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.T("common.progress.running", map[string]any{"Task": "security checks"})) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.T("common.progress.running", map[string]any{"Task": "security checks"}))
ctx := context.Background() ctx := context.Background()
result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{ result, err := RunSecurityChecks(ctx, SecurityOptions{
Dir: cwd, Dir: cwd,
Severity: securitySeverity, Severity: securitySeverity,
JSON: securityJSONOutput, JSON: securityJSONOutput,
@ -440,7 +440,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
} }
if result.Summary.Critical > 0 || result.Summary.High > 0 { if result.Summary.Critical > 0 || result.Summary.High > 0 {
return fmt.Errorf(i18n.T("cmd.php.error.critical_high_issues")) return errors.New(i18n.T("cmd.php.error.critical_high_issues"))
} }
return nil return nil
@ -472,18 +472,18 @@ func addPHPQACommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Determine stages // Determine stages
opts := phppkg.QAOptions{ opts := QAOptions{
Dir: cwd, Dir: cwd,
Quick: qaQuick, Quick: qaQuick,
Full: qaFull, Full: qaFull,
Fix: qaFix, Fix: qaFix,
} }
stages := phppkg.GetQAStages(opts) stages := GetQAStages(opts)
// Print header // Print header
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline"))
@ -567,9 +567,9 @@ func addPHPQACommand(parent *cobra.Command) {
} }
// getCheckStage determines which stage a check belongs to. // getCheckStage determines which stage a check belongs to.
func getCheckStage(checkName string, stages []phppkg.QAStage, dir string) string { func getCheckStage(checkName string, stages []QAStage, dir string) string {
for _, stage := range stages { for _, stage := range stages {
checks := phppkg.GetQAChecks(dir, stage) checks := GetQAChecks(dir, stage)
for _, c := range checks { for _, c := range checks {
if c == checkName { if c == checkName {
return string(stage) return string(stage)
@ -622,16 +622,16 @@ func addPHPRectorCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Check if Rector is available // Check if Rector is available
if !phppkg.DetectRector(cwd) { if !DetectRector(cwd) {
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.rector.not_found")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.rector.not_found"))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.rector.install")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.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")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup"))
return fmt.Errorf(i18n.T("cmd.php.error.rector_not_installed")) return errors.New(i18n.T("cmd.php.error.rector_not_installed"))
} }
var msg string var msg string
@ -644,7 +644,7 @@ func addPHPRectorCommand(parent *cobra.Command) {
ctx := context.Background() ctx := context.Background()
opts := phppkg.RectorOptions{ opts := RectorOptions{
Dir: cwd, Dir: cwd,
Fix: rectorFix, Fix: rectorFix,
Diff: rectorDiff, Diff: rectorDiff,
@ -652,7 +652,7 @@ func addPHPRectorCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
} }
if err := phppkg.RunRector(ctx, opts); err != nil { if err := RunRector(ctx, opts); err != nil {
if rectorFix { if rectorFix {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err)
} }
@ -696,15 +696,15 @@ func addPHPInfectionCommand(parent *cobra.Command) {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
if !phppkg.IsPHPProject(cwd) { if !IsPHPProject(cwd) {
return fmt.Errorf(i18n.T("cmd.php.error.not_php")) return errors.New(i18n.T("cmd.php.error.not_php"))
} }
// Check if Infection is available // Check if Infection is available
if !phppkg.DetectInfection(cwd) { if !DetectInfection(cwd) {
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.infection.not_found")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.infection.not_found"))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.infection.install")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.infection.install"))
return fmt.Errorf(i18n.T("cmd.php.error.infection_not_installed")) 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.T("common.progress.running", map[string]any{"Task": "mutation testing"})) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.T("common.progress.running", map[string]any{"Task": "mutation testing"}))
@ -712,7 +712,7 @@ func addPHPInfectionCommand(parent *cobra.Command) {
ctx := context.Background() ctx := context.Background()
opts := phppkg.InfectionOptions{ opts := InfectionOptions{
Dir: cwd, Dir: cwd,
MinMSI: infectionMinMSI, MinMSI: infectionMinMSI,
MinCoveredMSI: infectionMinCoveredMSI, MinCoveredMSI: infectionMinCoveredMSI,
@ -722,7 +722,7 @@ func addPHPInfectionCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
} }
if err := phppkg.RunInfection(ctx, opts); err != nil { if err := RunInfection(ctx, opts); err != nil {
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err)
} }

View file

@ -1,4 +1,4 @@
// Package pkg provides GitHub package management for host-uk repositories. // Package pkgcmd provides GitHub package management for host-uk repositories.
// //
// Commands: // Commands:
// - search: Search GitHub org for repos (cached for 1 hour) // - search: Search GitHub org for repos (cached for 1 hour)
@ -9,11 +9,4 @@
// //
// Uses gh CLI for authenticated GitHub access. Results are cached in // Uses gh CLI for authenticated GitHub access. Results are cached in
// .core/cache/ within the workspace directory. // .core/cache/ within the workspace directory.
package pkg package pkgcmd
import "github.com/spf13/cobra"
// AddCommands registers the 'pkg' command and all subcommands.
func AddCommands(root *cobra.Command) {
AddPkgCommands(root)
}

View file

@ -1,7 +1,8 @@
package pkg package pkgcmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -25,7 +26,7 @@ func addPkgInstallCommand(parent *cobra.Command) {
Long: i18n.T("cmd.pkg.install.long"), Long: i18n.T("cmd.pkg.install.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.pkg.error.repo_required")) return errors.New(i18n.T("cmd.pkg.error.repo_required"))
} }
return runPkgInstall(args[0], installTargetDir, installAddToReg) return runPkgInstall(args[0], installTargetDir, installAddToReg)
}, },
@ -43,7 +44,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
// Parse org/repo // Parse org/repo
parts := strings.Split(repoArg, "/") parts := strings.Split(repoArg, "/")
if len(parts) != 2 { if len(parts) != 2 {
return fmt.Errorf(i18n.T("cmd.pkg.error.invalid_repo_format")) return errors.New(i18n.T("cmd.pkg.error.invalid_repo_format"))
} }
org, repoName := parts[0], parts[1] org, repoName := parts[0], parts[1]
@ -78,7 +79,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
} }
if err := os.MkdirAll(targetDir, 0755); err != nil { if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "create directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "create directory"}), err)
} }
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName) fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
@ -110,7 +111,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
func addToRegistryFile(org, repoName string) error { func addToRegistryFile(org, repoName string) error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)

View file

@ -1,6 +1,7 @@
package pkg package pkgcmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -29,12 +30,12 @@ func addPkgListCommand(parent *cobra.Command) {
func runPkgList() error { func runPkgList() error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml_workspace")) return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml_workspace"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err)
} }
basePath := reg.BasePath basePath := reg.BasePath
@ -101,7 +102,7 @@ func addPkgUpdateCommand(parent *cobra.Command) {
Long: i18n.T("cmd.pkg.update.long"), Long: i18n.T("cmd.pkg.update.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if !updateAll && len(args) == 0 { if !updateAll && len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.pkg.error.specify_package")) return errors.New(i18n.T("cmd.pkg.error.specify_package"))
} }
return runPkgUpdate(args, updateAll) return runPkgUpdate(args, updateAll)
}, },
@ -115,12 +116,12 @@ func addPkgUpdateCommand(parent *cobra.Command) {
func runPkgUpdate(packages []string, all bool) error { func runPkgUpdate(packages []string, all bool) error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err)
} }
basePath := reg.BasePath basePath := reg.BasePath
@ -195,12 +196,12 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
func runPkgOutdated() error { func runPkgOutdated() error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err)
} }
basePath := reg.BasePath basePath := reg.BasePath

View file

@ -1,5 +1,5 @@
// Package pkg provides package management commands for core-* repos. // Package pkgcmd provides package management commands for core-* repos.
package pkg package pkgcmd
import ( import (
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
@ -7,6 +7,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddPkgCommands)
}
// Style and utility aliases // Style and utility aliases
var ( var (
repoNameStyle = cli.RepoNameStyle repoNameStyle = cli.RepoNameStyle

View file

@ -1,7 +1,8 @@
package pkg package pkgcmd
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -93,7 +94,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
// Fetch from GitHub if not cached // Fetch from GitHub if not cached
if !fromCache { if !fromCache {
if !ghAuthenticated() { if !ghAuthenticated() {
return fmt.Errorf(i18n.T("cmd.pkg.error.gh_not_authenticated")) return errors.New(i18n.T("cmd.pkg.error.gh_not_authenticated"))
} }
if os.Getenv("GH_TOKEN") != "" { if os.Getenv("GH_TOKEN") != "" {
@ -112,13 +113,13 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
fmt.Println() fmt.Println()
errStr := strings.TrimSpace(string(output)) errStr := strings.TrimSpace(string(output))
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") { if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
return fmt.Errorf(i18n.T("cmd.pkg.error.auth_failed")) return errors.New(i18n.T("cmd.pkg.error.auth_failed"))
} }
return fmt.Errorf(i18n.T("cmd.pkg.error.search_failed"), errStr) return fmt.Errorf("%s: %s", i18n.T("cmd.pkg.error.search_failed"), errStr)
} }
if err := json.Unmarshal(output, &ghRepos); err != nil { if err := json.Unmarshal(output, &ghRepos); err != nil {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "parse results"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "parse results"}), err)
} }
if c != nil { if c != nil {
@ -149,7 +150,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
return filtered[i].Name < filtered[j].Name return filtered[i].Name < filtered[j].Name
}) })
fmt.Printf(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n") fmt.Print(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n")
for _, r := range filtered { for _, r := range filtered {
visibility := "" visibility := ""

8
pkg/sdk/cmd_commands.go Normal file
View file

@ -0,0 +1,8 @@
// SDK validation and API compatibility commands.
//
// Commands:
// - diff: Check for breaking API changes between spec versions
// - validate: Validate OpenAPI spec syntax
//
// Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk
package sdk

View file

@ -1,16 +1,19 @@
// Package sdk provides SDK validation and API compatibility commands.
package sdk package sdk
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
sdkpkg "github.com/host-uk/core/pkg/sdk"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddSDKCommands)
}
// SDK styles (aliases to shared) // SDK styles (aliases to shared)
var ( var (
sdkHeaderStyle = cli.TitleStyle sdkHeaderStyle = cli.TitleStyle
@ -48,7 +51,7 @@ var sdkValidateCmd = &cobra.Command{
}, },
} }
func init() { func initSDKCommands() {
// sdk diff flags // sdk diff flags
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base")) sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base"))
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec")) sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec"))
@ -61,6 +64,12 @@ func init() {
sdkCmd.AddCommand(sdkValidateCmd) sdkCmd.AddCommand(sdkValidateCmd)
} }
// AddSDKCommands registers the 'sdk' command and all subcommands.
func AddSDKCommands(root *cobra.Command) {
initSDKCommands()
root.AddCommand(sdkCmd)
}
func runSDKDiff(basePath, specPath string) error { func runSDKDiff(basePath, specPath string) error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
@ -69,7 +78,7 @@ func runSDKDiff(basePath, specPath string) error {
// Detect current spec if not provided // Detect current spec if not provided
if specPath == "" { if specPath == "" {
s := sdkpkg.New(projectDir, nil) s := New(projectDir, nil)
specPath, err = s.DetectSpec() specPath, err = s.DetectSpec()
if err != nil { if err != nil {
return err return err
@ -77,7 +86,7 @@ func runSDKDiff(basePath, specPath string) error {
} }
if basePath == "" { if basePath == "" {
return fmt.Errorf(i18n.T("cmd.sdk.diff.error.base_required")) return errors.New(i18n.T("cmd.sdk.diff.error.base_required"))
} }
fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("common.progress.checking", map[string]any{"Item": "breaking changes"})) fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("common.progress.checking", map[string]any{"Item": "breaking changes"}))
@ -85,7 +94,7 @@ func runSDKDiff(basePath, specPath string) error {
fmt.Printf(" %s %s\n", i18n.T("common.label.current"), sdkDimStyle.Render(specPath)) fmt.Printf(" %s %s\n", i18n.T("common.label.current"), sdkDimStyle.Render(specPath))
fmt.Println() fmt.Println()
result, err := sdkpkg.Diff(basePath, specPath) result, err := Diff(basePath, specPath)
if err != nil { if err != nil {
fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("common.label.error")), err) fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("common.label.error")), err)
os.Exit(2) os.Exit(2)
@ -109,7 +118,7 @@ func runSDKValidate(specPath string) error {
return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err)
} }
s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath}) s := New(projectDir, &Config{Spec: specPath})
fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.label.sdk")), i18n.T("cmd.sdk.validate.validating")) fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.label.sdk")), i18n.T("cmd.sdk.validate.validating"))

View file

@ -1,4 +1,4 @@
// setup_bootstrap.go implements bootstrap mode for new workspaces. // cmd_bootstrap.go implements bootstrap mode for new workspaces.
// //
// Bootstrap mode is activated when no repos.yaml exists in the current // Bootstrap mode is activated when no repos.yaml exists in the current
// directory or any parent. It clones core-devops first, then uses its // directory or any parent. It clones core-devops first, then uses its

View file

@ -23,9 +23,16 @@
// Uses gh CLI with HTTPS when authenticated, falls back to SSH. // Uses gh CLI with HTTPS when authenticated, falls back to SSH.
package setup package setup
import "github.com/spf13/cobra" import (
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// AddCommands registers the 'setup' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddSetupCommands)
}
// AddSetupCommands registers the 'setup' command and all subcommands.
func AddSetupCommands(root *cobra.Command) {
AddSetupCommand(root) AddSetupCommand(root)
} }

View file

@ -1,4 +1,4 @@
// setup_registry.go implements registry mode for cloning packages. // cmd_registry.go implements registry mode for cloning packages.
// //
// Registry mode is activated when a repos.yaml exists. It reads the registry // Registry mode is activated when a repos.yaml exists. It reads the registry
// and clones all (or selected) packages into the configured packages directory. // and clones all (or selected) packages into the configured packages directory.

View file

@ -1,4 +1,4 @@
// setup_repo.go implements repository setup with .core/ configuration. // cmd_repo.go implements repository setup with .core/ configuration.
// //
// When running setup in an existing git repository, this generates // When running setup in an existing git repository, this generates
// build.yaml, release.yaml, and test.yaml configurations based on // build.yaml, release.yaml, and test.yaml configurations based on

View file

@ -41,7 +41,7 @@ var setupCmd = &cobra.Command{
}, },
} }
func init() { func initSetupFlags() {
setupCmd.Flags().StringVar(&registryPath, "registry", "", i18n.T("cmd.setup.flag.registry")) setupCmd.Flags().StringVar(&registryPath, "registry", "", i18n.T("cmd.setup.flag.registry"))
setupCmd.Flags().StringVar(&only, "only", "", i18n.T("cmd.setup.flag.only")) setupCmd.Flags().StringVar(&only, "only", "", i18n.T("cmd.setup.flag.only"))
setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, i18n.T("cmd.setup.flag.dry_run")) setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, i18n.T("cmd.setup.flag.dry_run"))
@ -52,5 +52,6 @@ func init() {
// AddSetupCommand adds the 'setup' command to the given parent command. // AddSetupCommand adds the 'setup' command to the given parent command.
func AddSetupCommand(root *cobra.Command) { func AddSetupCommand(root *cobra.Command) {
initSetupFlags()
root.AddCommand(setupCmd) root.AddCommand(setupCmd)
} }

View file

@ -1,4 +1,4 @@
// setup_wizard.go implements the interactive package selection wizard. // cmd_wizard.go implements the interactive package selection wizard.
// //
// Uses charmbracelet/huh for a rich terminal UI with multi-select checkboxes. // Uses charmbracelet/huh for a rich terminal UI with multi-select checkboxes.
// Falls back to non-interactive mode when not in a TTY or --all is specified. // Falls back to non-interactive mode when not in a TTY or --all is specified.

View file

@ -11,9 +11,8 @@
// Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json // Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json
package testcmd package testcmd
import "github.com/spf13/cobra" import "github.com/host-uk/core/pkg/cli"
// AddCommands registers the 'test' command and all subcommands. func init() {
func AddCommands(root *cobra.Command) { cli.RegisterCommands(AddTestCommands)
root.AddCommand(testCmd)
} }

View file

@ -41,7 +41,7 @@ var testCmd = &cobra.Command{
}, },
} }
func init() { func initTestFlags() {
testCmd.Flags().BoolVar(&testVerbose, "verbose", false, i18n.T("cmd.test.flag.verbose")) testCmd.Flags().BoolVar(&testVerbose, "verbose", false, i18n.T("cmd.test.flag.verbose"))
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("common.flag.coverage")) testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("common.flag.coverage"))
testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.test.flag.short")) testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.test.flag.short"))
@ -50,3 +50,9 @@ func init() {
testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.test.flag.race")) testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.test.flag.race"))
testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.test.flag.json")) testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.test.flag.json"))
} }
// AddTestCommands registers the 'test' command and all subcommands.
func AddTestCommands(root *cobra.Command) {
initTestFlags()
root.AddCommand(testCmd)
}

View file

@ -2,6 +2,7 @@ package testcmd
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -15,7 +16,7 @@ import (
func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error { func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error {
// Detect if we're in a Go project // Detect if we're in a Go project
if _, err := os.Stat("go.mod"); os.IsNotExist(err) { if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
return fmt.Errorf(i18n.T("cmd.test.error.no_go_mod")) return errors.New(i18n.T("cmd.test.error.no_go_mod"))
} }
// Build command arguments // Build command arguments
@ -93,7 +94,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
// JSON output for CI/agents // JSON output for CI/agents
printJSONResults(results, exitCode) printJSONResults(results, exitCode)
if exitCode != 0 { if exitCode != 0 {
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) return errors.New(i18n.T("common.error.failed", map[string]any{"Action": "run tests"}))
} }
return nil return nil
} }
@ -109,7 +110,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
if exitCode != 0 { if exitCode != 0 {
fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed")) fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed"))
return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) return errors.New(i18n.T("common.error.failed", map[string]any{"Action": "run tests"}))
} }
fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed")) fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed"))

View file

@ -11,10 +11,3 @@
// Uses qemu or hyperkit depending on system availability. // Uses qemu or hyperkit depending on system availability.
// Templates are built from YAML definitions and can include variables. // Templates are built from YAML definitions and can include variables.
package vm package vm
import "github.com/spf13/cobra"
// AddCommands registers the 'vm' command and all subcommands.
func AddCommands(root *cobra.Command) {
AddVMCommands(root)
}

View file

@ -2,6 +2,7 @@ package vm
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -47,7 +48,7 @@ func addVMRunCommand(parent *cobra.Command) {
// Otherwise, require an image path // Otherwise, require an image path
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.vm.run.error.image_required")) return errors.New(i18n.T("cmd.vm.run.error.image_required"))
} }
image := args[0] image := args[0]
@ -210,7 +211,7 @@ func addVMStopCommand(parent *cobra.Command) {
Long: i18n.T("cmd.vm.stop.long"), Long: i18n.T("cmd.vm.stop.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.vm.error.id_required")) return errors.New(i18n.T("cmd.vm.error.id_required"))
} }
return stopContainer(args[0]) return stopContainer(args[0])
}, },
@ -259,11 +260,11 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s
switch len(matches) { switch len(matches) {
case 0: case 0:
return "", fmt.Errorf(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID})) return "", errors.New(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID}))
case 1: case 1:
return matches[0].ID, nil return matches[0].ID, nil
default: default:
return "", fmt.Errorf(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID})) return "", errors.New(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID}))
} }
} }
@ -277,7 +278,7 @@ func addVMLogsCommand(parent *cobra.Command) {
Long: i18n.T("cmd.vm.logs.long"), Long: i18n.T("cmd.vm.logs.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.vm.error.id_required")) return errors.New(i18n.T("cmd.vm.error.id_required"))
} }
return viewLogs(args[0], logsFollow) return viewLogs(args[0], logsFollow)
}, },
@ -318,7 +319,7 @@ func addVMExecCommand(parent *cobra.Command) {
Long: i18n.T("cmd.vm.exec.long"), Long: i18n.T("cmd.vm.exec.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf(i18n.T("cmd.vm.error.id_and_cmd_required")) return errors.New(i18n.T("cmd.vm.error.id_and_cmd_required"))
} }
return execInContainer(args[0], args[1:]) return execInContainer(args[0], args[1:])
}, },

View file

@ -2,6 +2,7 @@ package vm
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -40,7 +41,7 @@ func addTemplatesShowCommand(parent *cobra.Command) {
Long: i18n.T("cmd.vm.templates.show.long"), Long: i18n.T("cmd.vm.templates.show.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.vm.error.template_required")) return errors.New(i18n.T("cmd.vm.error.template_required"))
} }
return showTemplate(args[0]) return showTemplate(args[0])
}, },
@ -57,7 +58,7 @@ func addTemplatesVarsCommand(parent *cobra.Command) {
Long: i18n.T("cmd.vm.templates.vars.long"), Long: i18n.T("cmd.vm.templates.vars.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf(i18n.T("cmd.vm.error.template_required")) return errors.New(i18n.T("cmd.vm.error.template_required"))
} }
return showTemplateVars(args[0]) return showTemplateVars(args[0])
}, },
@ -177,7 +178,7 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
// Find the built image (linuxkit creates .iso or other format) // Find the built image (linuxkit creates .iso or other format)
imagePath := findBuiltImage(outputPath) imagePath := findBuiltImage(outputPath)
if imagePath == "" { if imagePath == "" {
return fmt.Errorf(i18n.T("cmd.vm.error.no_image_found")) return errors.New(i18n.T("cmd.vm.error.no_image_found"))
} }
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.image")), imagePath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.image")), imagePath)
@ -286,7 +287,7 @@ func lookupLinuxKit() (string, error) {
} }
} }
return "", fmt.Errorf(i18n.T("cmd.vm.error.linuxkit_not_found")) return "", errors.New(i18n.T("cmd.vm.error.linuxkit_not_found"))
} }
// ParseVarFlags parses --var flags into a map. // ParseVarFlags parses --var flags into a map.

View file

@ -8,6 +8,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
cli.RegisterCommands(AddVMCommands)
}
// Style aliases from shared // Style aliases from shared
var ( var (
repoNameStyle = cli.RepoNameStyle repoNameStyle = cli.RepoNameStyle