refactor(cli): move cmd/shared to pkg/cli with runtime

Moves shared utilities (styles, utils) from cmd/shared to pkg/cli.
Adds CLI runtime with global singleton pattern:
- cli.Init() initialises the runtime
- cli.App() returns the global instance
- OutputService for styled terminal printing
- SignalService for graceful shutdown handling

All cmd/ packages now import pkg/cli instead of cmd/shared.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 10:32:05 +00:00
parent 6ed025d3e6
commit 23d399407c
31 changed files with 406 additions and 208 deletions

View file

@ -3,35 +3,35 @@
package ai
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// Style aliases from shared package
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
truncate = shared.Truncate
formatAge = shared.FormatAge
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
truncate = cli.Truncate
formatAge = cli.FormatAge
)
// Task priority/status styles from shared
var (
taskPriorityHighStyle = shared.PriorityHighStyle
taskPriorityMediumStyle = shared.PriorityMediumStyle
taskPriorityLowStyle = shared.PriorityLowStyle
taskStatusPendingStyle = shared.StatusPendingStyle
taskStatusInProgressStyle = shared.StatusRunningStyle
taskStatusCompletedStyle = shared.StatusSuccessStyle
taskStatusBlockedStyle = shared.StatusErrorStyle
taskPriorityHighStyle = cli.PriorityHighStyle
taskPriorityMediumStyle = cli.PriorityMediumStyle
taskPriorityLowStyle = cli.PriorityLowStyle
taskStatusPendingStyle = cli.StatusPendingStyle
taskStatusInProgressStyle = cli.StatusRunningStyle
taskStatusCompletedStyle = cli.StatusSuccessStyle
taskStatusBlockedStyle = cli.StatusErrorStyle
)
// Task-specific styles (aliases to shared where possible)
var (
taskIDStyle = shared.TitleStyle // Bold + blue
taskTitleStyle = shared.ValueStyle // Light gray
taskLabelStyle = shared.AccentLabelStyle // Violet for labels
taskIDStyle = cli.TitleStyle // Bold + blue
taskTitleStyle = cli.ValueStyle // Light gray
taskLabelStyle = cli.AccentLabelStyle // Violet for labels
)
// AddAgenticCommands adds the agentic task management commands to the ai command.

View file

@ -4,18 +4,18 @@ package build
import (
"embed"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared package
var (
buildHeaderStyle = shared.TitleStyle
buildTargetStyle = shared.ValueStyle
buildSuccessStyle = shared.SuccessStyle
buildErrorStyle = shared.ErrorStyle
buildDimStyle = shared.DimStyle
buildHeaderStyle = cli.TitleStyle
buildTargetStyle = cli.ValueStyle
buildSuccessStyle = cli.SuccessStyle
buildErrorStyle = cli.ErrorStyle
buildDimStyle = cli.DimStyle
)
//go:embed all:tmpl/gui

View file

@ -2,18 +2,18 @@
package ci
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared
var (
releaseHeaderStyle = shared.RepoNameStyle
releaseSuccessStyle = shared.SuccessStyle
releaseErrorStyle = shared.ErrorStyle
releaseDimStyle = shared.DimStyle
releaseValueStyle = shared.ValueStyle
releaseHeaderStyle = cli.RepoNameStyle
releaseSuccessStyle = cli.SuccessStyle
releaseErrorStyle = cli.ErrorStyle
releaseDimStyle = cli.DimStyle
releaseValueStyle = cli.ValueStyle
)
// Flag variables for ci command

View file

@ -19,17 +19,17 @@ package cmd
import (
"os"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/spf13/cobra"
)
// Terminal styles using Tailwind colour palette (from shared package).
var (
// coreStyle is used for primary headings and the CLI name.
coreStyle = shared.RepoNameStyle
coreStyle = cli.RepoNameStyle
// linkStyle is used for URLs and clickable references.
linkStyle = shared.LinkStyle
linkStyle = cli.LinkStyle
)
// rootCmd is the base command for the CLI.

View file

@ -29,27 +29,27 @@
package dev
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared package
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
warningStyle = shared.WarningStyle
dimStyle = shared.DimStyle
valueStyle = shared.ValueStyle
headerStyle = shared.HeaderStyle
repoNameStyle = shared.RepoNameStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
warningStyle = cli.WarningStyle
dimStyle = cli.DimStyle
valueStyle = cli.ValueStyle
headerStyle = cli.HeaderStyle
repoNameStyle = cli.RepoNameStyle
)
// Table styles for status display (extends shared styles with cell padding)
var (
dirtyStyle = shared.GitDirtyStyle.Padding(0, 1)
aheadStyle = shared.GitAheadStyle.Padding(0, 1)
cleanStyle = shared.GitCleanStyle.Padding(0, 1)
dirtyStyle = cli.GitDirtyStyle.Padding(0, 1)
aheadStyle = cli.GitAheadStyle.Padding(0, 1)
cleanStyle = cli.GitCleanStyle.Padding(0, 1)
)
// AddCommands registers the 'dev' command and all subcommands.

View file

@ -8,7 +8,7 @@ import (
"strings"
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
@ -16,10 +16,10 @@ import (
// CI-specific styles (aliases to shared)
var (
ciSuccessStyle = shared.SuccessStyle
ciFailureStyle = shared.ErrorStyle
ciPendingStyle = shared.StatusWarningStyle
ciSkippedStyle = shared.DimStyle
ciSuccessStyle = cli.SuccessStyle
ciFailureStyle = cli.ErrorStyle
ciPendingStyle = cli.StatusWarningStyle
ciSkippedStyle = cli.DimStyle
)
// WorkflowRun represents a GitHub Actions workflow run
@ -246,10 +246,10 @@ func printWorkflowRun(run WorkflowRun) {
}
// Workflow name (truncated)
workflowName := shared.Truncate(run.Name, 20)
workflowName := cli.Truncate(run.Name, 20)
// Age
age := shared.FormatAge(run.UpdatedAt)
age := cli.FormatAge(run.UpdatedAt)
fmt.Printf(" %s %-18s %-22s %s\n",
status,

View file

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
@ -128,7 +128,7 @@ func runCommit(registryPath string, all bool) error {
// Confirm unless --all
if !all {
fmt.Println()
if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
@ -207,7 +207,7 @@ func runCommitSingleRepo(ctx context.Context, repoPath string, all bool) error {
// Confirm unless --all
if !all {
fmt.Println()
if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
if !cli.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}

View file

@ -6,7 +6,7 @@ import (
"os"
"sort"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
@ -144,36 +144,36 @@ func runHealth(registryPath string, verbose bool) error {
func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
parts := []string{
shared.StatusPart(total, i18n.T("cmd.dev.health.repos"), shared.ValueStyle),
cli.StatusPart(total, i18n.T("cmd.dev.health.repos"), cli.ValueStyle),
}
// Dirty status
if len(dirty) > 0 {
parts = append(parts, shared.StatusPart(len(dirty), i18n.T("cmd.dev.health.dirty"), shared.WarningStyle))
parts = append(parts, cli.StatusPart(len(dirty), i18n.T("cmd.dev.health.dirty"), cli.WarningStyle))
} else {
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.status.clean"), shared.SuccessStyle))
parts = append(parts, cli.StatusText(i18n.T("cmd.dev.status.clean"), cli.SuccessStyle))
}
// Push status
if len(ahead) > 0 {
parts = append(parts, shared.StatusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), shared.ValueStyle))
parts = append(parts, cli.StatusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), cli.ValueStyle))
} else {
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.synced"), shared.SuccessStyle))
parts = append(parts, cli.StatusText(i18n.T("cmd.dev.health.synced"), cli.SuccessStyle))
}
// Pull status
if len(behind) > 0 {
parts = append(parts, shared.StatusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), shared.WarningStyle))
parts = append(parts, cli.StatusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), cli.WarningStyle))
} else {
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.up_to_date"), shared.SuccessStyle))
parts = append(parts, cli.StatusText(i18n.T("cmd.dev.health.up_to_date"), cli.SuccessStyle))
}
// Errors (only if any)
if len(errors) > 0 {
parts = append(parts, shared.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), shared.ErrorStyle))
parts = append(parts, cli.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), cli.ErrorStyle))
}
fmt.Println(shared.StatusLine(parts...))
fmt.Println(cli.StatusLine(parts...))
}
func formatRepoList(reposList []string) string {

View file

@ -4,7 +4,7 @@ import (
"fmt"
"sort"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
@ -12,9 +12,9 @@ import (
// Impact-specific styles (aliases to shared)
var (
impactDirectStyle = shared.ErrorStyle
impactIndirectStyle = shared.StatusWarningStyle
impactSafeStyle = shared.StatusSuccessStyle
impactDirectStyle = cli.ErrorStyle
impactIndirectStyle = cli.StatusWarningStyle
impactSafeStyle = cli.StatusSuccessStyle
)
// Impact command flags
@ -112,7 +112,7 @@ func runImpact(registryPath string, repoName string) error {
r, _ := reg.Get(d)
desc := ""
if r != nil && r.Description != "" {
desc = dimStyle.Render(" - " + shared.Truncate(r.Description, 40))
desc = dimStyle.Render(" - " + cli.Truncate(r.Description, 40))
}
fmt.Printf(" %s%s\n", d, desc)
}
@ -129,7 +129,7 @@ func runImpact(registryPath string, repoName string) error {
r, _ := reg.Get(d)
desc := ""
if r != nil && r.Description != "" {
desc = dimStyle.Render(" - " + shared.Truncate(r.Description, 40))
desc = dimStyle.Render(" - " + cli.Truncate(r.Description, 40))
}
fmt.Printf(" %s%s\n", d, desc)
}

View file

@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
@ -17,12 +17,12 @@ import (
// Issue-specific styles (aliases to shared)
var (
issueRepoStyle = shared.DimStyle
issueNumberStyle = shared.TitleStyle
issueTitleStyle = shared.ValueStyle
issueLabelStyle = shared.WarningStyle
issueAssigneeStyle = shared.StatusSuccessStyle
issueAgeStyle = shared.DimStyle
issueRepoStyle = cli.DimStyle
issueNumberStyle = cli.TitleStyle
issueTitleStyle = cli.ValueStyle
issueLabelStyle = cli.WarningStyle
issueAssigneeStyle = cli.StatusSuccessStyle
issueAgeStyle = cli.DimStyle
)
// GitHubIssue represents a GitHub issue from the API.
@ -201,7 +201,7 @@ func printIssue(issue GitHubIssue) {
// #42 [core-bio] Fix avatar upload
num := issueNumberStyle.Render(fmt.Sprintf("#%d", issue.Number))
repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", issue.RepoName))
title := issueTitleStyle.Render(shared.Truncate(issue.Title, 60))
title := issueTitleStyle.Render(cli.Truncate(issue.Title, 60))
line := fmt.Sprintf(" %s %s %s", num, repo, title)
@ -224,7 +224,7 @@ func printIssue(issue GitHubIssue) {
}
// Add age
age := shared.FormatAge(issue.CreatedAt)
age := cli.FormatAge(issue.CreatedAt)
line += " " + issueAgeStyle.Render(age)
fmt.Println(line)

View file

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
@ -122,7 +122,7 @@ func runPush(registryPath string, force bool) error {
// Confirm unless --force
if !force {
fmt.Println()
if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
@ -161,7 +161,7 @@ func runPush(registryPath string, force bool) error {
if len(divergedRepos) > 0 {
fmt.Println()
fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if shared.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
fmt.Println()
for _, r := range divergedRepos {
fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), r.Name)
@ -226,7 +226,7 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error {
}
fmt.Println()
fmt.Println()
if shared.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) {
if cli.Confirm(i18n.T("cmd.dev.push.uncommitted_changes_commit")) {
fmt.Println()
// Use edit-enabled commit if only untracked files (may need .gitignore fix)
var err error
@ -260,7 +260,7 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error {
// Confirm unless --force
if !force {
fmt.Println()
if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) {
if !cli.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": s.Ahead, "Repos": 1})) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
@ -275,7 +275,7 @@ func runPushSingleRepo(ctx context.Context, repoPath string, force bool) error {
fmt.Printf(" %s %s: %s\n", warningStyle.Render("!"), repoName, i18n.T("cmd.dev.push.diverged"))
fmt.Println()
fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if shared.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
fmt.Println()
fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), repoName)
if pullErr := git.Pull(ctx, repoPath); pullErr != nil {

View file

@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
@ -17,13 +17,13 @@ import (
// PR-specific styles (aliases to shared)
var (
prNumberStyle = shared.PrNumberStyle
prTitleStyle = shared.ValueStyle
prAuthorStyle = shared.InfoStyle
prApprovedStyle = shared.SuccessStyle
prChangesStyle = shared.WarningStyle
prPendingStyle = shared.DimStyle
prDraftStyle = shared.DimStyle
prNumberStyle = cli.PrNumberStyle
prTitleStyle = cli.ValueStyle
prAuthorStyle = cli.InfoStyle
prApprovedStyle = cli.SuccessStyle
prChangesStyle = cli.WarningStyle
prPendingStyle = cli.DimStyle
prDraftStyle = cli.DimStyle
)
// GitHubPR represents a GitHub pull request.
@ -234,7 +234,7 @@ func printPR(pr GitHubPR) {
// #12 [core-php] Webhook validation
num := prNumberStyle.Render(fmt.Sprintf("#%d", pr.Number))
repo := issueRepoStyle.Render(fmt.Sprintf("[%s]", pr.RepoName))
title := prTitleStyle.Render(shared.Truncate(pr.Title, 50))
title := prTitleStyle.Render(cli.Truncate(pr.Title, 50))
author := prAuthorStyle.Render("@" + pr.Author.Login)
// Review status
@ -254,7 +254,7 @@ func printPR(pr GitHubPR) {
draft = prDraftStyle.Render(" " + i18n.T("cmd.dev.reviews.draft"))
}
age := shared.FormatAge(pr.CreatedAt)
age := cli.FormatAge(pr.CreatedAt)
fmt.Printf(" %s %s %s%s %s %s %s\n", num, repo, title, draft, author, status, issueAgeStyle.Render(age))
}

View file

@ -8,7 +8,7 @@ import (
"sort"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
@ -109,7 +109,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Auto-commit dirty repos if requested
if autoCommit && len(dirtyRepos) > 0 {
fmt.Println()
fmt.Printf("%s\n", shared.TitleStyle.Render(i18n.T("cmd.dev.commit.committing")))
fmt.Printf("%s\n", cli.TitleStyle.Render(i18n.T("cmd.dev.commit.committing")))
fmt.Println()
for _, s := range dirtyRepos {
@ -168,7 +168,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
}
fmt.Println()
if !shared.Confirm(i18n.T("cmd.dev.push.confirm")) {
if !cli.Confirm(i18n.T("cmd.dev.push.confirm")) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
@ -203,7 +203,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
if len(divergedRepos) > 0 {
fmt.Println()
fmt.Printf("%s\n", i18n.T("cmd.dev.push.diverged_help"))
if shared.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
if cli.Confirm(i18n.T("cmd.dev.push.pull_and_retry")) {
fmt.Println()
for _, s := range divergedRepos {
fmt.Printf(" %s %s...\n", dimStyle.Render("↓"), s.Name)
@ -244,11 +244,11 @@ func printStatusTable(statuses []git.RepoStatus) {
// Print header with fixed-width formatting
fmt.Printf("%-*s %8s %9s %6s %5s\n",
nameWidth,
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_repo")),
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_modified")),
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_untracked")),
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_staged")),
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_ahead")),
cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_repo")),
cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_modified")),
cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_untracked")),
cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_staged")),
cli.TitleStyle.Render(i18n.T("cmd.dev.work.table_ahead")),
)
// Print separator

View file

@ -2,22 +2,22 @@
package docs
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style and utility aliases from shared
var (
repoNameStyle = shared.RepoNameStyle
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
headerStyle = shared.HeaderStyle
confirm = shared.Confirm
docsFoundStyle = shared.SuccessStyle
docsMissingStyle = shared.DimStyle
docsFileStyle = shared.InfoStyle
repoNameStyle = cli.RepoNameStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
headerStyle = cli.HeaderStyle
confirm = cli.Confirm
docsFoundStyle = cli.SuccessStyle
docsMissingStyle = cli.DimStyle
docsFileStyle = cli.InfoStyle
)
var docsCmd = &cobra.Command{

View file

@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -44,11 +44,11 @@ func runDocsList(registryPath string) error {
for _, repo := range reg.List() {
info := scanRepoDocs(repo)
readme := shared.CheckMark(info.Readme != "")
claude := shared.CheckMark(info.ClaudeMd != "")
changelog := shared.CheckMark(info.Changelog != "")
readme := cli.CheckMark(info.Readme != "")
claude := cli.CheckMark(info.ClaudeMd != "")
changelog := cli.CheckMark(info.Changelog != "")
docsDir := shared.CheckMark(false)
docsDir := cli.CheckMark(false)
if len(info.DocsFiles) > 0 {
docsDir = docsFoundStyle.Render(i18n.T("cmd.docs.list.files_count", map[string]interface{}{"Count": len(info.DocsFiles)}))
}
@ -70,7 +70,7 @@ func runDocsList(registryPath string) error {
fmt.Println()
fmt.Printf("%s %s\n",
shared.Label(i18n.T("cmd.docs.list.coverage_label")),
cli.Label(i18n.T("cmd.docs.list.coverage_label")),
i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}),
)

View file

@ -4,16 +4,16 @@ package doctor
import (
"fmt"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
)
// Flag variable for doctor command
@ -44,13 +44,13 @@ func runDoctor(verbose bool) error {
ok, version := runCheck(c)
if ok {
if verbose {
fmt.Println(shared.CheckResult(true, c.name, version))
fmt.Println(cli.CheckResult(true, c.name, version))
} else {
fmt.Println(shared.CheckResult(true, c.name, ""))
fmt.Println(cli.CheckResult(true, c.name, ""))
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", errorStyle.Render(shared.SymbolCross), c.name, c.description)
fmt.Printf(" %s %s - %s\n", errorStyle.Render(cli.SymbolCross), c.name, c.description)
failed++
}
}
@ -61,13 +61,13 @@ func runDoctor(verbose bool) error {
ok, version := runCheck(c)
if ok {
if verbose {
fmt.Println(shared.CheckResult(true, c.name, version))
fmt.Println(cli.CheckResult(true, c.name, version))
} else {
fmt.Println(shared.CheckResult(true, c.name, ""))
fmt.Println(cli.CheckResult(true, c.name, ""))
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", dimStyle.Render(shared.SymbolSkip), c.name, dimStyle.Render(c.description))
fmt.Printf(" %s %s - %s\n", dimStyle.Render(cli.SymbolSkip), c.name, dimStyle.Render(c.description))
optional++
}
}
@ -75,16 +75,16 @@ func runDoctor(verbose bool) error {
// Check GitHub access
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github"))
if checkGitHubSSH() {
fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
fmt.Println(cli.CheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
} else {
fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.ssh_missing"))
fmt.Printf(" %s %s\n", errorStyle.Render(cli.SymbolCross), i18n.T("cmd.doctor.ssh_missing"))
failed++
}
if checkGitHubCLI() {
fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
fmt.Println(cli.CheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
} else {
fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.cli_auth_missing"))
fmt.Printf(" %s %s\n", errorStyle.Render(cli.SymbolCross), i18n.T("cmd.doctor.cli_auth_missing"))
failed++
}
@ -95,12 +95,12 @@ func runDoctor(verbose bool) error {
// Summary
fmt.Println()
if failed > 0 {
fmt.Println(shared.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed})))
fmt.Println(cli.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed})))
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing"))
printInstallInstructions()
return fmt.Errorf("%s", i18n.T("cmd.doctor.issues_error", map[string]interface{}{"Count": failed}))
}
fmt.Println(shared.Success(i18n.T("cmd.doctor.ready")))
fmt.Println(cli.Success(i18n.T("cmd.doctor.ready")))
return nil
}

View file

@ -4,16 +4,16 @@
package gocmd
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases for shared styles
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
)
// AddGoCommands adds Go development commands.

View file

@ -8,7 +8,7 @@ import (
"regexp"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -119,7 +119,7 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
}
if cov > 0 {
fmt.Printf("\n %s %s\n", shared.ProgressLabel(i18n.T("cmd.go.test.coverage")), shared.FormatCoverage(cov))
fmt.Printf("\n %s %s\n", cli.ProgressLabel(i18n.T("cmd.go.test.coverage")), cli.FormatCoverage(cov))
}
if err == nil {
@ -241,7 +241,7 @@ func addGoCovCommand(parent *cobra.Command) {
// Print coverage summary
fmt.Println()
fmt.Printf(" %s %s\n", shared.ProgressLabel(i18n.T("label.total")), shared.FormatCoverage(totalCov))
fmt.Printf(" %s %s\n", cli.ProgressLabel(i18n.T("label.total")), cli.FormatCoverage(totalCov))
// Generate HTML if requested
if covHTML || covOpen {

View file

@ -3,49 +3,49 @@ package php
import (
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared
var (
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
linkStyle = shared.LinkStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
linkStyle = cli.LinkStyle
)
// Service colors for log output (domain-specific, keep local)
var (
phpFrankenPHPStyle = lipgloss.NewStyle().Foreground(shared.ColourIndigo500)
phpViteStyle = lipgloss.NewStyle().Foreground(shared.ColourYellow500)
phpHorizonStyle = lipgloss.NewStyle().Foreground(shared.ColourOrange500)
phpReverbStyle = lipgloss.NewStyle().Foreground(shared.ColourViolet500)
phpRedisStyle = lipgloss.NewStyle().Foreground(shared.ColourRed500)
phpFrankenPHPStyle = lipgloss.NewStyle().Foreground(cli.ColourIndigo500)
phpViteStyle = lipgloss.NewStyle().Foreground(cli.ColourYellow500)
phpHorizonStyle = lipgloss.NewStyle().Foreground(cli.ColourOrange500)
phpReverbStyle = lipgloss.NewStyle().Foreground(cli.ColourViolet500)
phpRedisStyle = lipgloss.NewStyle().Foreground(cli.ColourRed500)
)
// Status styles (from shared)
var (
phpStatusRunning = shared.SuccessStyle
phpStatusStopped = shared.StatusPendingStyle
phpStatusError = shared.ErrorStyle
phpStatusRunning = cli.SuccessStyle
phpStatusStopped = cli.StatusPendingStyle
phpStatusError = cli.ErrorStyle
)
// QA command styles (from shared)
var (
phpQAPassedStyle = shared.SuccessStyle
phpQAFailedStyle = shared.ErrorStyle
phpQAWarningStyle = shared.WarningStyle
phpQAStageStyle = shared.StageStyle
phpQAPassedStyle = cli.SuccessStyle
phpQAFailedStyle = cli.ErrorStyle
phpQAWarningStyle = cli.WarningStyle
phpQAStageStyle = cli.StageStyle
)
// Security severity styles (from shared)
var (
phpSecurityCriticalStyle = shared.SeverityCriticalStyle
phpSecurityHighStyle = shared.SeverityHighStyle
phpSecurityMediumStyle = shared.SeverityMediumStyle
phpSecurityLowStyle = shared.SeverityLowStyle
phpSecurityCriticalStyle = cli.SeverityCriticalStyle
phpSecurityHighStyle = cli.SeverityHighStyle
phpSecurityMediumStyle = cli.SeverityMediumStyle
phpSecurityLowStyle = cli.SeverityLowStyle
)
// AddPHPCommands adds PHP/Laravel development commands.

View file

@ -6,7 +6,7 @@ import (
"os"
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra"
@ -14,9 +14,9 @@ import (
// Deploy command styles (aliases to shared)
var (
phpDeployStyle = shared.DeploySuccessStyle
phpDeployPendingStyle = shared.StatusWarningStyle
phpDeployFailedStyle = shared.StatusErrorStyle
phpDeployStyle = cli.DeploySuccessStyle
phpDeployPendingStyle = cli.StatusWarningStyle
phpDeployFailedStyle = cli.StatusErrorStyle
)
func addPHPDeployCommands(parent *cobra.Command) {

View file

@ -2,19 +2,19 @@
package pkg
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style and utility aliases
var (
repoNameStyle = shared.RepoNameStyle
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
ghAuthenticated = shared.GhAuthenticated
gitClone = shared.GitClone
repoNameStyle = cli.RepoNameStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
ghAuthenticated = cli.GhAuthenticated
gitClone = cli.GitClone
)
// AddPkgCommands adds the 'pkg' command and subcommands for package management.

View file

@ -5,7 +5,7 @@ import (
"fmt"
"os"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
sdkpkg "github.com/host-uk/core/pkg/sdk"
"github.com/spf13/cobra"
@ -13,10 +13,10 @@ import (
// SDK styles (aliases to shared)
var (
sdkHeaderStyle = shared.TitleStyle
sdkSuccessStyle = shared.SuccessStyle
sdkErrorStyle = shared.ErrorStyle
sdkDimStyle = shared.DimStyle
sdkHeaderStyle = cli.TitleStyle
sdkSuccessStyle = cli.SuccessStyle
sdkErrorStyle = cli.ErrorStyle
sdkDimStyle = cli.DimStyle
)
var sdkCmd = &cobra.Command{

View file

@ -2,17 +2,17 @@
package setup
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared package
var (
repoNameStyle = shared.RepoNameStyle
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
repoNameStyle = cli.RepoNameStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
)
// Default organization and devops repo for bootstrap

View file

@ -13,7 +13,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
)
@ -216,7 +216,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
// gitClone clones a repository using gh CLI or git.
func gitClone(ctx context.Context, org, repo, path string) error {
// Try gh clone first with HTTPS (works without SSH keys)
if shared.GhAuthenticated() {
if cli.GhAuthenticated() {
// Use HTTPS URL directly to bypass git_protocol config
httpsURL := fmt.Sprintf("https://github.com/%s/%s.git", org, repo)
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", httpsURL, path)

View file

@ -12,7 +12,7 @@ import (
"strings"
"github.com/charmbracelet/huh"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"golang.org/x/term"
@ -157,7 +157,7 @@ func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string,
var selected []string
// Header styling
headerStyle := shared.TitleStyle.MarginBottom(1)
headerStyle := cli.TitleStyle.MarginBottom(1)
fmt.Println(headerStyle.Render(i18n.T("cmd.setup.wizard.package_selection")))
fmt.Println(i18n.T("cmd.setup.wizard.selection_hint"))

View file

@ -4,21 +4,21 @@
package testcmd
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared
var (
testHeaderStyle = shared.RepoNameStyle
testPassStyle = shared.SuccessStyle
testFailStyle = shared.ErrorStyle
testSkipStyle = shared.WarningStyle
testDimStyle = shared.DimStyle
testCovHighStyle = shared.CoverageHighStyle
testCovMedStyle = shared.CoverageMedStyle
testCovLowStyle = shared.CoverageLowStyle
testHeaderStyle = cli.RepoNameStyle
testPassStyle = cli.SuccessStyle
testFailStyle = cli.ErrorStyle
testSkipStyle = cli.WarningStyle
testDimStyle = cli.DimStyle
testCovHighStyle = cli.CoverageHighStyle
testCovMedStyle = cli.CoverageMedStyle
testCovLowStyle = cli.CoverageLowStyle
)
// Flag variables for test command

View file

@ -9,7 +9,7 @@ import (
"strconv"
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
)
@ -153,7 +153,7 @@ func printCoverageSummary(results testResults) {
}
func formatCoverage(cov float64) string {
return shared.FormatCoverage(cov)
return cli.FormatCoverage(cov)
}
func shortenPackageName(name string) string {

View file

@ -3,23 +3,23 @@ package vm
import (
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/cli"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
// Style aliases from shared
var (
repoNameStyle = shared.RepoNameStyle
successStyle = shared.SuccessStyle
errorStyle = shared.ErrorStyle
dimStyle = shared.DimStyle
repoNameStyle = cli.RepoNameStyle
successStyle = cli.SuccessStyle
errorStyle = cli.ErrorStyle
dimStyle = cli.DimStyle
)
// VM-specific styles
var (
varStyle = lipgloss.NewStyle().Foreground(shared.ColourAmber500)
defaultStyle = lipgloss.NewStyle().Foreground(shared.ColourGray500).Italic(true)
varStyle = lipgloss.NewStyle().Foreground(cli.ColourAmber500)
defaultStyle = lipgloss.NewStyle().Foreground(cli.ColourGray500).Italic(true)
)
// AddVMCommands adds container-related commands under 'vm' to the CLI.

198
pkg/cli/runtime.go Normal file
View file

@ -0,0 +1,198 @@
// Package cli provides the CLI runtime and utilities.
//
// The CLI uses the Core framework for its own runtime, providing:
// - Global singleton access via cli.App()
// - Output service for styled terminal printing
// - Signal handling for graceful shutdown
// - Worker bundle spawning for commands
package cli
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"github.com/host-uk/core/pkg/framework"
)
var (
instance *Runtime
once sync.Once
)
// Runtime is the CLI's Core runtime.
type Runtime struct {
Core *framework.Core
ctx context.Context
cancel context.CancelFunc
}
// RuntimeOptions configures the CLI runtime.
type RuntimeOptions struct {
// AppName is the CLI application name (used in output)
AppName string
// Version is the CLI version string
Version string
}
// Init initialises the global CLI runtime.
// Call this once at startup (typically in main.go).
func Init(opts RuntimeOptions) error {
var initErr error
once.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
core, err := framework.New(
framework.WithService(NewOutputService(OutputServiceOptions{
AppName: opts.AppName,
})),
framework.WithService(NewSignalService(SignalServiceOptions{
Cancel: cancel,
})),
framework.WithServiceLock(),
)
if err != nil {
initErr = err
cancel()
return
}
instance = &Runtime{
Core: core,
ctx: ctx,
cancel: cancel,
}
// Start services
if err := core.ServiceStartup(ctx, nil); err != nil {
initErr = err
return
}
})
return initErr
}
// App returns the global CLI runtime.
// Panics if Init() hasn't been called.
func App() *Runtime {
if instance == nil {
panic("cli.App() called before cli.Init()")
}
return instance
}
// Context returns the CLI's root context.
// This context is cancelled on shutdown signals.
func (r *Runtime) Context() context.Context {
return r.ctx
}
// Shutdown gracefully shuts down the CLI runtime.
func (r *Runtime) Shutdown() {
r.cancel()
r.Core.ServiceShutdown(r.ctx)
}
// Output returns the output service for styled printing.
func (r *Runtime) Output() *OutputService {
return framework.MustServiceFor[*OutputService](r.Core, "output")
}
// --- Output Service ---
// OutputServiceOptions configures the output service.
type OutputServiceOptions struct {
AppName string
}
// OutputService provides styled terminal output.
type OutputService struct {
*framework.ServiceRuntime[OutputServiceOptions]
}
// NewOutputService creates an output service factory.
func NewOutputService(opts OutputServiceOptions) func(*framework.Core) (any, error) {
return func(c *framework.Core) (any, error) {
return &OutputService{
ServiceRuntime: framework.NewServiceRuntime(c, opts),
}, nil
}
}
// Success prints a success message with checkmark.
func (s *OutputService) Success(msg string) {
fmt.Println(SuccessStyle.Render(SymbolCheck + " " + msg))
}
// Error prints an error message with cross.
func (s *OutputService) Error(msg string) {
fmt.Println(ErrorStyle.Render(SymbolCross + " " + msg))
}
// Warning prints a warning message.
func (s *OutputService) Warning(msg string) {
fmt.Println(WarningStyle.Render(SymbolWarning + " " + msg))
}
// Info prints an info message.
func (s *OutputService) Info(msg string) {
fmt.Println(InfoStyle.Render(SymbolInfo + " " + msg))
}
// Title prints a title/header.
func (s *OutputService) Title(msg string) {
fmt.Println(TitleStyle.Render(msg))
}
// Dim prints dimmed/subtle text.
func (s *OutputService) Dim(msg string) {
fmt.Println(DimStyle.Render(msg))
}
// --- Signal Service ---
// SignalServiceOptions configures the signal service.
type SignalServiceOptions struct {
Cancel context.CancelFunc
}
// SignalService handles OS signals for graceful shutdown.
type SignalService struct {
*framework.ServiceRuntime[SignalServiceOptions]
sigChan chan os.Signal
}
// NewSignalService creates a signal service factory.
func NewSignalService(opts SignalServiceOptions) func(*framework.Core) (any, error) {
return func(c *framework.Core) (any, error) {
return &SignalService{
ServiceRuntime: framework.NewServiceRuntime(c, opts),
sigChan: make(chan os.Signal, 1),
}, nil
}
}
// OnStartup starts listening for signals.
func (s *SignalService) OnStartup(ctx context.Context) error {
signal.Notify(s.sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
select {
case <-s.sigChan:
s.Opts().Cancel()
case <-ctx.Done():
}
}()
return nil
}
// OnShutdown stops listening for signals.
func (s *SignalService) OnShutdown(ctx context.Context) error {
signal.Stop(s.sigChan)
close(s.sigChan)
return nil
}

View file

@ -1,11 +1,11 @@
// Package shared provides common utilities and styles for CLI commands.
// Package cli provides common utilities and styles for CLI commands.
//
// This package contains:
// - Terminal styling using lipgloss with Tailwind colours
// - Unicode symbols for consistent visual indicators
// - Helper functions for common output patterns
// - Git and GitHub CLI utilities
package shared
package cli
import (
"fmt"

View file

@ -1,4 +1,4 @@
package shared
package cli
import (
"context"