feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
All checks were successful
Security Scan / security (push) Successful in 13s

Replace fmt, errors, strings, path/filepath, encoding/json across 31
files. Migrate core.New() to functional-options API. Keep fmt.Sscanf,
strings.Index/Repeat/FieldsSeq, os, io (infra types).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-03-26 14:31:34 +00:00
parent e177418c90
commit e9e0eccd60
No known key found for this signature in database
GPG key ID: AF404715446AEB41
31 changed files with 393 additions and 386 deletions

View file

@ -1,8 +1,7 @@
package config
import (
"fmt"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
)
@ -20,7 +19,7 @@ func addGetCommand(parent *cli.Command) {
return cli.Err("key not found: %s", key)
}
fmt.Println(value)
core.Println(value)
return nil
})

View file

@ -1,9 +1,10 @@
package config
import (
"fmt"
"maps"
"os"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
"gopkg.in/yaml.v3"
)
@ -26,7 +27,7 @@ func addListCommand(parent *cli.Command) {
return cli.Wrap(err, "failed to format config")
}
fmt.Print(string(out))
core.Print(os.Stdout, "%s", string(out))
return nil
})

View file

@ -1,8 +1,7 @@
package config
import (
"fmt"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
)
@ -13,7 +12,7 @@ func addPathCommand(parent *cli.Command) {
return err
}
fmt.Println(cfg.Path())
core.Println(cfg.Path())
return nil
})

View file

@ -2,8 +2,8 @@ package doctor
import (
"os/exec"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
)
@ -93,9 +93,9 @@ func runCheck(c check) (bool, string) {
}
// Extract first line as version
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
lines := core.Split(core.Trim(string(output)), "\n")
if len(lines) > 0 {
return true, strings.TrimSpace(lines[0])
return true, core.Trim(lines[0])
}
return true, ""
}

View file

@ -2,9 +2,9 @@
package doctor
import (
"errors"
"fmt"
"os"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-i18n"
"github.com/spf13/cobra"
@ -32,72 +32,72 @@ func init() {
}
func runDoctor(verbose bool) error {
fmt.Println(i18n.T("common.progress.checking", map[string]any{"Item": "development environment"}))
fmt.Println()
core.Println(i18n.T("common.progress.checking", map[string]any{"Item": "development environment"}))
core.Println()
var passed, failed, optional int
// Check required tools
fmt.Println(i18n.T("cmd.doctor.required"))
core.Println(i18n.T("cmd.doctor.required"))
for _, c := range requiredChecks() {
ok, version := runCheck(c)
if ok {
if verbose {
fmt.Println(formatCheckResult(true, c.name, version))
core.Println(formatCheckResult(true, c.name, version))
} else {
fmt.Println(formatCheckResult(true, c.name, ""))
core.Println(formatCheckResult(true, c.name, ""))
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", errorStyle.Render(cli.Glyph(":cross:")), c.name, c.description)
core.Print(os.Stdout, " %s %s - %s\n", errorStyle.Render(cli.Glyph(":cross:")), c.name, c.description)
failed++
}
}
// Check optional tools
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.optional"))
core.Print(os.Stdout, "\n%s\n", i18n.T("cmd.doctor.optional"))
for _, c := range optionalChecks() {
ok, version := runCheck(c)
if ok {
if verbose {
fmt.Println(formatCheckResult(true, c.name, version))
core.Println(formatCheckResult(true, c.name, version))
} else {
fmt.Println(formatCheckResult(true, c.name, ""))
core.Println(formatCheckResult(true, c.name, ""))
}
passed++
} else {
fmt.Printf(" %s %s - %s\n", dimStyle.Render(cli.Glyph(":skip:")), c.name, dimStyle.Render(c.description))
core.Print(os.Stdout, " %s %s - %s\n", dimStyle.Render(cli.Glyph(":skip:")), c.name, dimStyle.Render(c.description))
optional++
}
}
// Check GitHub access
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github"))
core.Print(os.Stdout, "\n%s\n", i18n.T("cmd.doctor.github"))
if checkGitHubSSH() {
fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
core.Println(formatCheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
} else {
fmt.Printf(" %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.ssh_missing"))
core.Print(os.Stdout, " %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.ssh_missing"))
failed++
}
if checkGitHubCLI() {
fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
core.Println(formatCheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
} else {
fmt.Printf(" %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.cli_auth_missing"))
core.Print(os.Stdout, " %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), i18n.T("cmd.doctor.cli_auth_missing"))
failed++
}
// Check workspace
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.workspace"))
core.Print(os.Stdout, "\n%s\n", i18n.T("cmd.doctor.workspace"))
checkWorkspace()
// Summary
fmt.Println()
core.Println()
if failed > 0 {
cli.Error(i18n.T("cmd.doctor.issues", map[string]any{"Count": failed}))
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing"))
core.Print(os.Stdout, "\n%s\n", i18n.T("cmd.doctor.install_missing"))
printInstallInstructions()
return errors.New(i18n.T("cmd.doctor.issues_error", map[string]any{"Count": failed}))
return core.NewError(i18n.T("cmd.doctor.issues_error", map[string]any{"Count": failed}))
}
cli.Success(i18n.T("cmd.doctor.ready"))

View file

@ -1,12 +1,10 @@
package doctor
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/repos"
@ -21,11 +19,11 @@ func checkGitHubSSH() bool {
return false
}
sshDir := filepath.Join(home, ".ssh")
sshDir := core.JoinPath(home, ".ssh")
keyPatterns := []string{"id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"}
for _, key := range keyPatterns {
keyPath := filepath.Join(sshDir, key)
keyPath := core.JoinPath(sshDir, key)
if _, err := os.Stat(keyPath); err == nil {
return true
}
@ -39,14 +37,14 @@ func checkGitHubCLI() bool {
cmd := exec.Command("gh", "auth", "status")
output, _ := cmd.CombinedOutput()
// Check for any successful login (even if there's also a failing token)
return strings.Contains(string(output), "Logged in to")
return core.Contains(string(output), "Logged in to")
}
// checkWorkspace checks for repos.yaml and counts cloned repos
func checkWorkspace() {
registryPath, err := repos.FindRegistry(io.Local)
if err == nil {
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_yaml_found", map[string]any{"Path": registryPath}))
core.Print(os.Stdout, " %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_yaml_found", map[string]any{"Path": registryPath}))
reg, err := repos.LoadRegistry(io.Local, registryPath)
if err == nil {
@ -54,26 +52,26 @@ func checkWorkspace() {
if basePath == "" {
basePath = "./packages"
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(registryPath), basePath)
if !core.PathIsAbs(basePath) {
basePath = core.JoinPath(core.PathDir(registryPath), basePath)
}
if strings.HasPrefix(basePath, "~/") {
if core.HasPrefix(basePath, "~/") {
home, _ := os.UserHomeDir()
basePath = filepath.Join(home, basePath[2:])
basePath = core.JoinPath(home, basePath[2:])
}
// Count existing repos
allRepos := reg.List()
var cloned int
for _, repo := range allRepos {
repoPath := filepath.Join(basePath, repo.Name)
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
repoPath := core.JoinPath(basePath, repo.Name)
if _, err := os.Stat(core.JoinPath(repoPath, ".git")); err == nil {
cloned++
}
}
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_cloned", map[string]any{"Cloned": cloned, "Total": len(allRepos)}))
core.Print(os.Stdout, " %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_cloned", map[string]any{"Cloned": cloned, "Total": len(allRepos)}))
}
} else {
fmt.Printf(" %s %s\n", dimStyle.Render("○"), i18n.T("cmd.doctor.no_repos_yaml"))
core.Print(os.Stdout, " %s %s\n", dimStyle.Render("○"), i18n.T("cmd.doctor.no_repos_yaml"))
}
}

View file

@ -1,9 +1,10 @@
package doctor
import (
"fmt"
"os"
"runtime"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
)
@ -11,16 +12,16 @@ import (
func printInstallInstructions() {
switch runtime.GOOS {
case "darwin":
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos_cask"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_macos"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_macos_cask"))
case "linux":
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_header"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_git"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_gh"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_php"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_node"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_pnpm"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_header"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_git"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_gh"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_php"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_node"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_pnpm"))
default:
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_other"))
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_other"))
}
}

View file

@ -1,8 +1,9 @@
package help
import (
"fmt"
"os"
"dappco.re/go/core"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-help"
)
@ -19,28 +20,28 @@ func AddHelpCommands(root *cli.Command) {
if searchFlag != "" {
results := catalog.Search(searchFlag)
if len(results) == 0 {
fmt.Println("No topics found.")
core.Println("No topics found.")
return
}
fmt.Println("Search Results:")
core.Println("Search Results:")
for _, res := range results {
fmt.Printf(" %s - %s\n", res.Topic.ID, res.Topic.Title)
core.Print(os.Stdout, " %s - %s\n", res.Topic.ID, res.Topic.Title)
}
return
}
if len(args) == 0 {
topics := catalog.List()
fmt.Println("Available Help Topics:")
core.Println("Available Help Topics:")
for _, t := range topics {
fmt.Printf(" %s - %s\n", t.ID, t.Title)
core.Print(os.Stdout, " %s - %s\n", t.ID, t.Title)
}
return
}
topic, err := catalog.Get(args[0])
if err != nil {
fmt.Printf("Error: %v\n", err)
core.Print(os.Stdout, "Error: %v\n", err)
return
}
@ -55,8 +56,8 @@ func AddHelpCommands(root *cli.Command) {
func renderTopic(t *help.Topic) {
// Simple ANSI rendering for now
// Use explicit ANSI codes or just print
fmt.Printf("\n\033[1;34m%s\033[0m\n", t.Title) // Blue bold title
fmt.Println("----------------------------------------")
fmt.Println(t.Content)
fmt.Println()
core.Print(os.Stdout, "\n\033[1;34m%s\033[0m\n", t.Title) // Blue bold title
core.Println("----------------------------------------")
core.Println(t.Content)
core.Println()
}

View file

@ -2,21 +2,15 @@ package pkgcmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
coreio "forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/repos"
"github.com/spf13/cobra"
)
import (
"errors"
)
var (
installTargetDir string
installAddToReg bool
@ -30,7 +24,7 @@ func addPkgInstallCommand(parent *cobra.Command) {
Long: i18n.T("cmd.pkg.install.long"),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New(i18n.T("cmd.pkg.error.repo_required"))
return core.NewError(i18n.T("cmd.pkg.error.repo_required"))
}
return runPkgInstall(args[0], installTargetDir, installAddToReg)
},
@ -46,9 +40,9 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
ctx := context.Background()
// Parse org/repo
parts := strings.Split(repoArg, "/")
parts := core.Split(repoArg, "/")
if len(parts) != 2 {
return errors.New(i18n.T("cmd.pkg.error.invalid_repo_format"))
return core.NewError(i18n.T("cmd.pkg.error.invalid_repo_format"))
}
org, repoName := parts[0], parts[1]
@ -60,8 +54,8 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
if targetDir == "" {
targetDir = "./packages"
}
if !filepath.IsAbs(targetDir) {
targetDir = filepath.Join(filepath.Dir(regPath), targetDir)
if !core.PathIsAbs(targetDir) {
targetDir = core.JoinPath(core.PathDir(regPath), targetDir)
}
}
}
@ -70,44 +64,44 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
}
}
if strings.HasPrefix(targetDir, "~/") {
if core.HasPrefix(targetDir, "~/") {
home, _ := os.UserHomeDir()
targetDir = filepath.Join(home, targetDir[2:])
targetDir = core.JoinPath(home, targetDir[2:])
}
repoPath := filepath.Join(targetDir, repoName)
repoPath := core.JoinPath(targetDir, repoName)
if coreio.Local.Exists(filepath.Join(repoPath, ".git")) {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("skip")), i18n.T("cmd.pkg.install.already_exists", map[string]string{"Name": repoName, "Path": repoPath}))
if coreio.Local.Exists(core.JoinPath(repoPath, ".git")) {
core.Print(os.Stdout, "%s %s\n", dimStyle.Render(i18n.Label("skip")), i18n.T("cmd.pkg.install.already_exists", map[string]string{"Name": repoName, "Path": repoPath}))
return nil
}
if err := coreio.Local.EnsureDir(targetDir); err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.create", "directory"), err)
return core.Wrap(err, "", i18n.T("i18n.fail.create", "directory"))
}
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("target")), repoPath)
fmt.Println()
core.Print(os.Stdout, "%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
core.Print(os.Stdout, "%s %s\n", dimStyle.Render(i18n.Label("target")), repoPath)
core.Println()
fmt.Printf(" %s... ", dimStyle.Render(i18n.T("common.status.cloning")))
core.Print(os.Stdout, " %s... ", dimStyle.Render(i18n.T("common.status.cloning")))
err := gitClone(ctx, org, repoName, repoPath)
if err != nil {
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
core.Print(os.Stdout, "%s\n", errorStyle.Render("✗ "+err.Error()))
return err
}
fmt.Printf("%s\n", successStyle.Render("✓"))
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
if addToRegistry {
if err := addToRegistryFile(org, repoName); err != nil {
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), i18n.T("cmd.pkg.install.add_to_registry"), err)
core.Print(os.Stdout, " %s %s: %s\n", errorStyle.Render("✗"), i18n.T("cmd.pkg.install.add_to_registry"), err)
} else {
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.pkg.install.added_to_registry"))
core.Print(os.Stdout, " %s %s\n", successStyle.Render("✓"), i18n.T("cmd.pkg.install.added_to_registry"))
}
}
fmt.Println()
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("i18n.done.install")), i18n.T("cmd.pkg.install.installed", map[string]string{"Name": repoName}))
core.Println()
core.Print(os.Stdout, "%s %s\n", successStyle.Render(i18n.T("i18n.done.install")), i18n.T("cmd.pkg.install.installed", map[string]string{"Name": repoName}))
return nil
}
@ -115,7 +109,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
func addToRegistryFile(org, repoName string) error {
regPath, err := repos.FindRegistry(coreio.Local)
if err != nil {
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
return core.NewError(i18n.T("cmd.pkg.error.no_repos_yaml"))
}
reg, err := repos.LoadRegistry(coreio.Local, regPath)
@ -133,7 +127,7 @@ func addToRegistryFile(org, repoName string) error {
}
repoType := detectRepoType(repoName)
entry := fmt.Sprintf("\n %s:\n type: %s\n description: (installed via core pkg install)\n",
entry := core.Sprintf("\n %s:\n type: %s\n description: (installed via core pkg install)\n",
repoName, repoType)
content += entry
@ -141,20 +135,20 @@ func addToRegistryFile(org, repoName string) error {
}
func detectRepoType(name string) string {
lower := strings.ToLower(name)
if strings.Contains(lower, "-mod-") || strings.HasSuffix(lower, "-mod") {
lower := core.Lower(name)
if core.Contains(lower, "-mod-") || core.HasSuffix(lower, "-mod") {
return "module"
}
if strings.Contains(lower, "-plug-") || strings.HasSuffix(lower, "-plug") {
if core.Contains(lower, "-plug-") || core.HasSuffix(lower, "-plug") {
return "plugin"
}
if strings.Contains(lower, "-services-") || strings.HasSuffix(lower, "-services") {
if core.Contains(lower, "-services-") || core.HasSuffix(lower, "-services") {
return "service"
}
if strings.Contains(lower, "-website-") || strings.HasSuffix(lower, "-website") {
if core.Contains(lower, "-website-") || core.HasSuffix(lower, "-website") {
return "website"
}
if strings.HasPrefix(lower, "core-") {
if core.HasPrefix(lower, "core-") {
return "package"
}
return "package"

View file

@ -1,12 +1,10 @@
package pkgcmd
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
coreio "forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/repos"
@ -30,34 +28,34 @@ func addPkgListCommand(parent *cobra.Command) {
func runPkgList() error {
regPath, err := repos.FindRegistry(coreio.Local)
if err != nil {
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml_workspace"))
return core.NewError(i18n.T("cmd.pkg.error.no_repos_yaml_workspace"))
}
reg, err := repos.LoadRegistry(coreio.Local, regPath)
if err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err)
return core.Wrap(err, "", i18n.T("i18n.fail.load", "registry"))
}
basePath := reg.BasePath
if basePath == "" {
basePath = "."
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(regPath), basePath)
if !core.PathIsAbs(basePath) {
basePath = core.JoinPath(core.PathDir(regPath), basePath)
}
allRepos := reg.List()
if len(allRepos) == 0 {
fmt.Println(i18n.T("cmd.pkg.list.no_packages"))
core.Println(i18n.T("cmd.pkg.list.no_packages"))
return nil
}
fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title")))
core.Print(os.Stdout, "%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title")))
var installed, missing int
for _, r := range allRepos {
repoPath := filepath.Join(basePath, r.Name)
exists := coreio.Local.Exists(filepath.Join(repoPath, ".git"))
repoPath := core.JoinPath(basePath, r.Name)
exists := coreio.Local.Exists(core.JoinPath(repoPath, ".git"))
if exists {
installed++
} else {
@ -77,15 +75,15 @@ func runPkgList() error {
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
}
fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name))
fmt.Printf(" %s\n", desc)
core.Print(os.Stdout, " %s %s\n", status, repoNameStyle.Render(r.Name))
core.Print(os.Stdout, " %s\n", desc)
}
fmt.Println()
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("total")), i18n.T("cmd.pkg.list.summary", map[string]int{"Installed": installed, "Missing": missing}))
core.Println()
core.Print(os.Stdout, "%s %s\n", dimStyle.Render(i18n.Label("total")), i18n.T("cmd.pkg.list.summary", map[string]int{"Installed": installed, "Missing": missing}))
if missing > 0 {
fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.list.install_missing"), dimStyle.Render("core setup"))
core.Print(os.Stdout, "\n%s %s\n", i18n.T("cmd.pkg.list.install_missing"), dimStyle.Render("core setup"))
}
return nil
@ -101,7 +99,7 @@ func addPkgUpdateCommand(parent *cobra.Command) {
Long: i18n.T("cmd.pkg.update.long"),
RunE: func(cmd *cobra.Command, args []string) error {
if !updateAll && len(args) == 0 {
return errors.New(i18n.T("cmd.pkg.error.specify_package"))
return core.NewError(i18n.T("cmd.pkg.error.specify_package"))
}
return runPkgUpdate(args, updateAll)
},
@ -115,20 +113,20 @@ func addPkgUpdateCommand(parent *cobra.Command) {
func runPkgUpdate(packages []string, all bool) error {
regPath, err := repos.FindRegistry(coreio.Local)
if err != nil {
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
return core.NewError(i18n.T("cmd.pkg.error.no_repos_yaml"))
}
reg, err := repos.LoadRegistry(coreio.Local, regPath)
if err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err)
return core.Wrap(err, "", i18n.T("i18n.fail.load", "registry"))
}
basePath := reg.BasePath
if basePath == "" {
basePath = "."
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(regPath), basePath)
if !core.PathIsAbs(basePath) {
basePath = core.JoinPath(core.PathDir(regPath), basePath)
}
var toUpdate []string
@ -140,39 +138,39 @@ func runPkgUpdate(packages []string, all bool) error {
toUpdate = packages
}
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)}))
core.Print(os.Stdout, "%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)}))
var updated, skipped, failed int
for _, name := range toUpdate {
repoPath := filepath.Join(basePath, name)
repoPath := core.JoinPath(basePath, name)
if _, err := coreio.Local.List(filepath.Join(repoPath, ".git")); err != nil {
fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
if _, err := coreio.Local.List(core.JoinPath(repoPath, ".git")); err != nil {
core.Print(os.Stdout, " %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
skipped++
continue
}
fmt.Printf(" %s %s... ", dimStyle.Render("↓"), name)
core.Print(os.Stdout, " %s %s... ", dimStyle.Render("↓"), name)
cmd := exec.Command("git", "-C", repoPath, "pull", "--ff-only")
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("%s\n", errorStyle.Render("✗"))
fmt.Printf(" %s\n", strings.TrimSpace(string(output)))
core.Print(os.Stdout, "%s\n", errorStyle.Render("✗"))
core.Print(os.Stdout, " %s\n", core.Trim(string(output)))
failed++
continue
}
if strings.Contains(string(output), "Already up to date") {
fmt.Printf("%s\n", dimStyle.Render(i18n.T("common.status.up_to_date")))
if core.Contains(string(output), "Already up to date") {
core.Print(os.Stdout, "%s\n", dimStyle.Render(i18n.T("common.status.up_to_date")))
} else {
fmt.Printf("%s\n", successStyle.Render("✓"))
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
}
updated++
}
fmt.Println()
fmt.Printf("%s %s\n",
core.Println()
core.Print(os.Stdout, "%s %s\n",
dimStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated, "Skipped": skipped, "Failed": failed}))
return nil
@ -195,30 +193,30 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
func runPkgOutdated() error {
regPath, err := repos.FindRegistry(coreio.Local)
if err != nil {
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
return core.NewError(i18n.T("cmd.pkg.error.no_repos_yaml"))
}
reg, err := repos.LoadRegistry(coreio.Local, regPath)
if err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err)
return core.Wrap(err, "", i18n.T("i18n.fail.load", "registry"))
}
basePath := reg.BasePath
if basePath == "" {
basePath = "."
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(regPath), basePath)
if !core.PathIsAbs(basePath) {
basePath = core.JoinPath(core.PathDir(regPath), basePath)
}
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("common.progress.checking_updates"))
core.Print(os.Stdout, "%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("common.progress.checking_updates"))
var outdated, upToDate, notInstalled int
for _, r := range reg.List() {
repoPath := filepath.Join(basePath, r.Name)
repoPath := core.JoinPath(basePath, r.Name)
if !coreio.Local.Exists(filepath.Join(repoPath, ".git")) {
if !coreio.Local.Exists(core.JoinPath(repoPath, ".git")) {
notInstalled++
continue
}
@ -233,9 +231,9 @@ func runPkgOutdated() error {
continue
}
count := strings.TrimSpace(string(output))
count := core.Trim(string(output))
if count != "0" {
fmt.Printf(" %s %s (%s)\n",
core.Print(os.Stdout, " %s %s (%s)\n",
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
outdated++
} else {
@ -243,13 +241,13 @@ func runPkgOutdated() error {
}
}
fmt.Println()
core.Println()
if outdated == 0 {
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
core.Print(os.Stdout, "%s %s\n", successStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
} else {
fmt.Printf("%s %s\n",
core.Print(os.Stdout, "%s %s\n",
dimStyle.Render(i18n.Label("summary")), i18n.T("cmd.pkg.outdated.summary", map[string]int{"Outdated": outdated, "UpToDate": upToDate}))
fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.outdated.update_with"), dimStyle.Render("core pkg update --all"))
core.Print(os.Stdout, "\n%s %s\n", i18n.T("cmd.pkg.outdated.update_with"), dimStyle.Render("core pkg update --all"))
}
return nil

View file

@ -8,12 +8,10 @@
package pkgcmd
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
coreio "forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-scm/repos"
@ -30,7 +28,7 @@ func addPkgRemoveCommand(parent *cobra.Command) {
changes or unpushed branches. Use --force to skip safety checks.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New(i18n.T("cmd.pkg.error.repo_required"))
return core.NewError(i18n.T("cmd.pkg.error.repo_required"))
}
return runPkgRemove(args[0], removeForce)
},
@ -45,49 +43,49 @@ func runPkgRemove(name string, force bool) error {
// Find package path via registry
regPath, err := repos.FindRegistry(coreio.Local)
if err != nil {
return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml"))
return core.NewError(i18n.T("cmd.pkg.error.no_repos_yaml"))
}
reg, err := repos.LoadRegistry(coreio.Local, regPath)
if err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.load", "registry"), err)
return core.Wrap(err, "", i18n.T("i18n.fail.load", "registry"))
}
basePath := reg.BasePath
if basePath == "" {
basePath = "."
}
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(filepath.Dir(regPath), basePath)
if !core.PathIsAbs(basePath) {
basePath = core.JoinPath(core.PathDir(regPath), basePath)
}
repoPath := filepath.Join(basePath, name)
repoPath := core.JoinPath(basePath, name)
if !coreio.Local.IsDir(filepath.Join(repoPath, ".git")) {
return fmt.Errorf("package %s is not installed at %s", name, repoPath)
if !coreio.Local.IsDir(core.JoinPath(repoPath, ".git")) {
return core.NewError(core.Sprintf("package %s is not installed at %s", name, repoPath))
}
if !force {
blocked, reasons := checkRepoSafety(repoPath)
if blocked {
fmt.Printf("%s Cannot remove %s:\n", errorStyle.Render("Blocked:"), repoNameStyle.Render(name))
core.Print(os.Stdout, "%s Cannot remove %s:\n", errorStyle.Render("Blocked:"), repoNameStyle.Render(name))
for _, r := range reasons {
fmt.Printf(" %s %s\n", errorStyle.Render("·"), r)
core.Print(os.Stdout, " %s %s\n", errorStyle.Render("·"), r)
}
fmt.Printf("\nResolve the issues above or use --force to override.\n")
return errors.New("package has unresolved changes")
core.Print(os.Stdout, "\nResolve the issues above or use --force to override.\n")
return core.NewError("package has unresolved changes")
}
}
// Remove the directory
fmt.Printf("%s %s... ", dimStyle.Render("Removing"), repoNameStyle.Render(name))
core.Print(os.Stdout, "%s %s... ", dimStyle.Render("Removing"), repoNameStyle.Render(name))
if err := coreio.Local.DeleteAll(repoPath); err != nil {
fmt.Printf("%s\n", errorStyle.Render("x "+err.Error()))
core.Print(os.Stdout, "%s\n", errorStyle.Render("x "+err.Error()))
return err
}
fmt.Printf("%s\n", successStyle.Render("ok"))
core.Print(os.Stdout, "%s\n", successStyle.Render("ok"))
return nil
}
@ -96,48 +94,48 @@ func checkRepoSafety(repoPath string) (blocked bool, reasons []string) {
// Check for uncommitted changes (staged, unstaged, untracked)
cmd := exec.Command("git", "-C", repoPath, "status", "--porcelain")
output, err := cmd.Output()
if err == nil && strings.TrimSpace(string(output)) != "" {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if err == nil && core.Trim(string(output)) != "" {
lines := core.Split(core.Trim(string(output)), "\n")
blocked = true
reasons = append(reasons, fmt.Sprintf("has %d uncommitted changes", len(lines)))
reasons = append(reasons, core.Sprintf("has %d uncommitted changes", len(lines)))
}
// Check for unpushed commits on current branch
cmd = exec.Command("git", "-C", repoPath, "log", "--oneline", "@{u}..HEAD")
output, err = cmd.Output()
if err == nil && strings.TrimSpace(string(output)) != "" {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if err == nil && core.Trim(string(output)) != "" {
lines := core.Split(core.Trim(string(output)), "\n")
blocked = true
reasons = append(reasons, fmt.Sprintf("has %d unpushed commits on current branch", len(lines)))
reasons = append(reasons, core.Sprintf("has %d unpushed commits on current branch", len(lines)))
}
// Check all local branches for unpushed work
cmd = exec.Command("git", "-C", repoPath, "branch", "--no-merged", "origin/HEAD")
output, _ = cmd.Output()
if trimmed := strings.TrimSpace(string(output)); trimmed != "" {
branches := strings.Split(trimmed, "\n")
if trimmed := core.Trim(string(output)); trimmed != "" {
branches := core.Split(trimmed, "\n")
var unmerged []string
for _, b := range branches {
b = strings.TrimSpace(b)
b = strings.TrimPrefix(b, "* ")
b = core.Trim(b)
b = core.TrimPrefix(b, "* ")
if b != "" {
unmerged = append(unmerged, b)
}
}
if len(unmerged) > 0 {
blocked = true
reasons = append(reasons, fmt.Sprintf("has %d unmerged branches: %s",
len(unmerged), strings.Join(unmerged, ", ")))
reasons = append(reasons, core.Sprintf("has %d unmerged branches: %s",
len(unmerged), core.Join(", ", unmerged...)))
}
}
// Check for stashed changes
cmd = exec.Command("git", "-C", repoPath, "stash", "list")
output, err = cmd.Output()
if err == nil && strings.TrimSpace(string(output)) != "" {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if err == nil && core.Trim(string(output)) != "" {
lines := core.Split(core.Trim(string(output)), "\n")
blocked = true
reasons = append(reasons, fmt.Sprintf("has %d stashed entries", len(lines)))
reasons = append(reasons, core.Sprintf("has %d stashed entries", len(lines)))
}
return blocked, reasons

View file

@ -3,16 +3,16 @@ package pkgcmd
import (
"os"
"os/exec"
"path/filepath"
"testing"
"dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestRepo(t *testing.T, dir, name string) string {
t.Helper()
repoPath := filepath.Join(dir, name)
repoPath := core.JoinPath(dir, name)
require.NoError(t, os.MkdirAll(repoPath, 0755))
cmds := [][]string{
@ -43,7 +43,7 @@ func TestCheckRepoSafety_UncommittedChanges(t *testing.T) {
tmp := t.TempDir()
repoPath := setupTestRepo(t, tmp, "dirty-repo")
require.NoError(t, os.WriteFile(filepath.Join(repoPath, "new.txt"), []byte("data"), 0644))
require.NoError(t, os.WriteFile(core.JoinPath(repoPath, "new.txt"), []byte("data"), 0644))
blocked, reasons := checkRepoSafety(repoPath)
assert.True(t, blocked)
@ -56,7 +56,7 @@ func TestCheckRepoSafety_Stash(t *testing.T) {
repoPath := setupTestRepo(t, tmp, "stash-repo")
// Create a file, add, stash
require.NoError(t, os.WriteFile(filepath.Join(repoPath, "stash.txt"), []byte("data"), 0644))
require.NoError(t, os.WriteFile(core.JoinPath(repoPath, "stash.txt"), []byte("data"), 0644))
cmd := exec.Command("git", "add", ".")
cmd.Dir = repoPath
require.NoError(t, cmd.Run())

View file

@ -2,16 +2,13 @@ package pkgcmd
import (
"cmp"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"time"
"dappco.re/go/core"
"forge.lthn.ai/core/go-cache"
"forge.lthn.ai/core/go-i18n"
coreio "forge.lthn.ai/core/go-io"
@ -72,7 +69,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
// Initialize cache in workspace .core/ directory
var cacheDir string
if regPath, err := repos.FindRegistry(coreio.Local); err == nil {
cacheDir = filepath.Join(filepath.Dir(regPath), ".core", "cache")
cacheDir = core.JoinPath(core.PathDir(regPath), ".core", "cache")
}
c, err := cache.New(coreio.Local, cacheDir, 0)
@ -89,46 +86,47 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
if found, err := c.Get(cacheKey, &ghRepos); found && err == nil {
fromCache = true
age := c.Age(cacheKey)
fmt.Printf("%s %s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.cache_label")), org, dimStyle.Render(fmt.Sprintf("(%s ago)", age.Round(time.Second))))
core.Print(os.Stdout, "%s %s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.cache_label")), org, dimStyle.Render(core.Sprintf("(%s ago)", age.Round(time.Second))))
}
}
// Fetch from GitHub if not cached
if !fromCache {
if !ghAuthenticated() {
return errors.New(i18n.T("cmd.pkg.error.gh_not_authenticated"))
return core.NewError(i18n.T("cmd.pkg.error.gh_not_authenticated"))
}
if os.Getenv("GH_TOKEN") != "" {
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("note")), i18n.T("cmd.pkg.search.gh_token_warning"))
fmt.Printf("%s %s\n\n", dimStyle.Render(""), i18n.T("cmd.pkg.search.gh_token_unset"))
core.Print(os.Stdout, "%s %s\n", dimStyle.Render(i18n.Label("note")), i18n.T("cmd.pkg.search.gh_token_warning"))
core.Print(os.Stdout, "%s %s\n\n", dimStyle.Render(""), i18n.T("cmd.pkg.search.gh_token_unset"))
}
fmt.Printf("%s %s... ", dimStyle.Render(i18n.T("cmd.pkg.search.fetching_label")), org)
core.Print(os.Stdout, "%s %s... ", dimStyle.Render(i18n.T("cmd.pkg.search.fetching_label")), org)
cmd := exec.Command("gh", "repo", "list", org,
"--json", "name,description,visibility,updatedAt,primaryLanguage",
"--limit", fmt.Sprintf("%d", limit))
"--limit", core.Sprintf("%d", limit))
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println()
errStr := strings.TrimSpace(string(output))
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
return errors.New(i18n.T("cmd.pkg.error.auth_failed"))
core.Println()
errStr := core.Trim(string(output))
if core.Contains(errStr, "401") || core.Contains(errStr, "Bad credentials") {
return core.NewError(i18n.T("cmd.pkg.error.auth_failed"))
}
return fmt.Errorf("%s: %s", i18n.T("cmd.pkg.error.search_failed"), errStr)
return core.NewError(core.Sprintf("%s: %s", i18n.T("cmd.pkg.error.search_failed"), errStr))
}
if err := json.Unmarshal(output, &ghRepos); err != nil {
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.parse", "results"), err)
r := core.JSONUnmarshal(output, &ghRepos)
if !r.OK {
return core.NewError(core.Sprintf("%s: %v", i18n.T("i18n.fail.parse", "results"), r.Value))
}
if c != nil {
_ = c.Set(cacheKey, ghRepos)
}
fmt.Printf("%s\n", successStyle.Render("✓"))
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
}
// Filter by glob pattern and type
@ -137,14 +135,14 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
if !matchGlob(pattern, r.Name) {
continue
}
if repoType != "" && !strings.Contains(r.Name, repoType) {
if repoType != "" && !core.Contains(r.Name, repoType) {
continue
}
filtered = append(filtered, r)
}
if len(filtered) == 0 {
fmt.Println(i18n.T("cmd.pkg.search.no_repos_found"))
core.Println(i18n.T("cmd.pkg.search.no_repos_found"))
return nil
}
@ -152,7 +150,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
return cmp.Compare(a.Name, b.Name)
})
fmt.Print(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n")
core.Print(os.Stdout, "%s", i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)})+"\n\n")
for _, r := range filtered {
visibility := ""
@ -168,12 +166,12 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
}
fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility)
fmt.Printf(" %s\n", desc)
core.Print(os.Stdout, " %s%s\n", repoNameStyle.Render(r.Name), visibility)
core.Print(os.Stdout, " %s\n", desc)
}
fmt.Println()
fmt.Printf("%s %s\n", i18n.T("common.hint.install_with"), dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org)))
core.Println()
core.Print(os.Stdout, "%s %s\n", i18n.T("common.hint.install_with"), dimStyle.Render(core.Sprintf("core pkg install %s/<repo-name>", org)))
return nil
}
@ -184,7 +182,7 @@ func matchGlob(pattern, name string) bool {
return true
}
parts := strings.Split(pattern, "*")
parts := core.Split(pattern, "*")
pos := 0
for i, part := range parts {
if part == "" {
@ -194,12 +192,12 @@ func matchGlob(pattern, name string) bool {
if idx == -1 {
return false
}
if i == 0 && !strings.HasPrefix(pattern, "*") && idx != 0 {
if i == 0 && !core.HasPrefix(pattern, "*") && idx != 0 {
return false
}
pos += idx + len(part)
}
if !strings.HasSuffix(pattern, "*") && pos != len(name) {
if !core.HasSuffix(pattern, "*") && pos != len(name) {
return false
}
return true

2
go.mod
View file

@ -2,7 +2,7 @@ module forge.lthn.ai/core/cli
go 1.26.0
require dappco.re/go/core v0.4.7
require dappco.re/go/core v0.8.0-alpha.1
require (
forge.lthn.ai/core/go-i18n v0.1.7

4
go.sum
View file

@ -1,5 +1,5 @@
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
forge.lthn.ai/core/go v0.3.2/go.mod h1:f7/zb3Labn4ARfwTq5Bi2AFHY+uxyPHozO+hLb54eFo=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=

View file

@ -1,11 +1,11 @@
package cli
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"dappco.re/go/core"
)
// ANSI escape codes
@ -134,24 +134,24 @@ func (s *AnsiStyle) Render(text string) string {
return text
}
return strings.Join(codes, "") + text + ansiReset
return core.Join("", codes...) + text + ansiReset
}
// fgColorHex converts a hex string to an ANSI foreground color code.
func fgColorHex(hex string) string {
r, g, b := hexToRGB(hex)
return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b)
return core.Sprintf("\033[38;2;%d;%d;%dm", r, g, b)
}
// bgColorHex converts a hex string to an ANSI background color code.
func bgColorHex(hex string) string {
r, g, b := hexToRGB(hex)
return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b)
return core.Sprintf("\033[48;2;%d;%d;%dm", r, g, b)
}
// hexToRGB converts a hex string to RGB values.
func hexToRGB(hex string) (int, int, int) {
hex = strings.TrimPrefix(hex, "#")
hex = core.TrimPrefix(hex, "#")
if len(hex) != 6 {
return 255, 255, 255
}

View file

@ -2,14 +2,13 @@ package cli
import (
"embed"
"fmt"
"io/fs"
"os"
"runtime/debug"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
"forge.lthn.ai/core/go-log"
"dappco.re/go/core"
"github.com/spf13/cobra"
)
@ -83,7 +82,7 @@ func MainWithLocales(locales []LocaleSource, commands ...CommandSetup) {
if r := recover(); r != nil {
log.Error("recovered from panic", "error", r, "stack", string(debug.Stack()))
Shutdown()
Fatal(fmt.Errorf("panic: %v", r))
Fatal(core.NewError(core.Sprintf("panic: %v", r)))
}
}()
@ -131,7 +130,7 @@ func newCompletionCmd() *cobra.Command {
return &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: fmt.Sprintf(`Generate shell completion script for the specified shell.
Long: core.Sprintf(`Generate shell completion script for the specified shell.
To load completions:

View file

@ -2,11 +2,11 @@ package cli
import (
"bytes"
"fmt"
"runtime/debug"
"sync"
"testing"
"dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
@ -47,7 +47,7 @@ func TestPanicRecovery_Good(t *testing.T) {
}
}()
panic(fmt.Errorf("error panic"))
panic(core.NewError(core.Sprintf("error panic")))
}()
err, ok := recovered.(error)
@ -91,7 +91,7 @@ func TestPanicRecovery_Bad(t *testing.T) {
}
}()
panic(fmt.Sprintf("panic from goroutine %d", id))
panic(core.Sprintf("panic from goroutine %d", id))
}(i)
}
@ -134,7 +134,7 @@ func TestMainPanicRecoveryPattern(t *testing.T) {
// Mock implementations
mockLogError := func(msg string, args ...any) {
fmt.Fprintf(&logBuffer, msg, args...)
core.Print(&logBuffer, msg, args...)
}
mockShutdown := func() {
shutdownCalled = true
@ -149,7 +149,7 @@ func TestMainPanicRecoveryPattern(t *testing.T) {
if r := recover(); r != nil {
mockLogError("recovered from panic: %v", r)
mockShutdown()
mockFatal(fmt.Errorf("panic: %v", r))
mockFatal(core.NewError(core.Sprintf("panic: %v", r)))
}
}()

View file

@ -1,10 +1,9 @@
package cli
import (
"errors"
"fmt"
"os"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
)
@ -15,7 +14,7 @@ import (
// Err creates a new error from a format string.
// This is a direct replacement for fmt.Errorf.
func Err(format string, args ...any) error {
return fmt.Errorf(format, args...)
return core.NewError(core.Sprintf(format, args...))
}
// Wrap wraps an error with a message.
@ -26,7 +25,7 @@ func Wrap(err error, msg string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", msg, err)
return core.Wrap(err, "", msg)
}
// WrapVerb wraps an error using i18n grammar for "Failed to verb subject".
@ -39,7 +38,7 @@ func WrapVerb(err error, verb, subject string) error {
return nil
}
msg := i18n.ActionFailed(verb, subject)
return fmt.Errorf("%s: %w", msg, err)
return core.Wrap(err, "", msg)
}
// WrapAction wraps an error using i18n grammar for "Failed to verb".
@ -52,7 +51,7 @@ func WrapAction(err error, verb string) error {
return nil
}
msg := i18n.ActionFailed(verb, "")
return fmt.Errorf("%s: %w", msg, err)
return core.Wrap(err, "", msg)
}
// ─────────────────────────────────────────────────────────────────────────────
@ -62,19 +61,19 @@ func WrapAction(err error, verb string) error {
// Is reports whether any error in err's tree matches target.
// This is a re-export of errors.Is for convenience.
func Is(err, target error) bool {
return errors.Is(err, target)
return core.Is(err, target)
}
// As finds the first error in err's tree that matches target.
// This is a re-export of errors.As for convenience.
func As(err error, target any) bool {
return errors.As(err, target)
return core.As(err, target)
}
// Join returns an error that wraps the given errors.
// This is a re-export of errors.Join for convenience.
func Join(errs ...error) error {
return errors.Join(errs...)
return core.ErrorJoin(errs...)
}
// ExitError represents an error that should cause the CLI to exit with a specific code.
@ -113,7 +112,7 @@ func Exit(code int, err error) error {
func Fatal(err error) {
if err != nil {
LogError("Fatal error", "err", err)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+err.Error()))
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+err.Error()))
os.Exit(1)
}
}
@ -122,9 +121,9 @@ func Fatal(err error) {
//
// Deprecated: return an error from the command instead.
func Fatalf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
msg := core.Sprintf(format, args...)
LogError("Fatal error", "msg", msg)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+msg))
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+msg))
os.Exit(1)
}
@ -139,8 +138,8 @@ func FatalWrap(err error, msg string) {
return
}
LogError("Fatal error", "msg", msg, "err", err)
fullMsg := fmt.Sprintf("%s: %v", msg, err)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
fullMsg := core.Sprintf("%s: %v", msg, err)
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
os.Exit(1)
}
@ -156,7 +155,7 @@ func FatalWrapVerb(err error, verb, subject string) {
}
msg := i18n.ActionFailed(verb, subject)
LogError("Fatal error", "msg", msg, "err", err, "verb", verb, "subject", subject)
fullMsg := fmt.Sprintf("%s: %v", msg, err)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
fullMsg := core.Sprintf("%s: %v", msg, err)
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
os.Exit(1)
}

View file

@ -1,13 +1,12 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"dappco.re/go/core"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
@ -397,7 +396,7 @@ func (f *Frame) updateFocusedLocked(msg tea.Msg) tea.Cmd {
// In non-TTY mode, it renders once and returns immediately.
func (f *Frame) Run() {
if !f.isTTY() {
fmt.Fprint(f.out, f.String())
_, _ = io.WriteString(f.out, f.String())
return
}
f.runLive()
@ -429,7 +428,7 @@ func (f *Frame) String() string {
return ""
}
// Ensure trailing newline for non-TTY consistency
if !strings.HasSuffix(view, "\n") {
if !core.HasSuffix(view, "\n") {
view += "\n"
}
return view

View file

@ -1,8 +1,9 @@
package cli
import (
"fmt"
"iter"
"dappco.re/go/core"
)
// Region represents one of the 5 HLCRF regions.
@ -91,7 +92,7 @@ func ParseVariant(variant string) (*Composite, error) {
for i < len(variant) {
r := Region(variant[i])
if !isValidRegion(r) {
return nil, fmt.Errorf("invalid region: %c", r)
return nil, core.NewError(core.Sprintf("invalid region: %c", r))
}
slot := &Slot{region: r, path: string(r)}
@ -101,7 +102,7 @@ func ParseVariant(variant string) (*Composite, error) {
if i < len(variant) && variant[i] == '[' {
end := findMatchingBracket(variant, i)
if end == -1 {
return nil, fmt.Errorf("unmatched bracket at %d", i)
return nil, core.NewError(core.Sprintf("unmatched bracket at %d", i))
}
nested, err := ParseVariant(variant[i+1 : end])
if err != nil {
@ -168,6 +169,6 @@ func toRenderable(item any) Renderable {
case string:
return StringBlock(v)
default:
return StringBlock(fmt.Sprint(v))
return StringBlock(core.Sprint(v))
}
}

View file

@ -1,60 +1,60 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
)
// Blank prints an empty line.
func Blank() {
fmt.Println()
core.Println()
}
// Echo translates a key via i18n.T and prints with newline.
// No automatic styling - use Success/Error/Warn/Info for styled output.
func Echo(key string, args ...any) {
fmt.Println(i18n.T(key, args...))
core.Println(i18n.T(key, args...))
}
// Print outputs formatted text (no newline).
// Glyph shortcodes like :check: are converted.
func Print(format string, args ...any) {
fmt.Print(compileGlyphs(fmt.Sprintf(format, args...)))
_, _ = io.WriteString(os.Stdout, compileGlyphs(core.Sprintf(format, args...)))
}
// Println outputs formatted text with newline.
// Glyph shortcodes like :check: are converted.
func Println(format string, args ...any) {
fmt.Println(compileGlyphs(fmt.Sprintf(format, args...)))
core.Println(compileGlyphs(core.Sprintf(format, args...)))
}
// Text prints arguments like fmt.Println, but handling glyphs.
func Text(args ...any) {
fmt.Println(compileGlyphs(fmt.Sprint(args...)))
core.Println(compileGlyphs(core.Sprint(args...)))
}
// Success prints a success message with checkmark (green).
func Success(msg string) {
fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg))
core.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg))
}
// Successf prints a formatted success message.
func Successf(format string, args ...any) {
Success(fmt.Sprintf(format, args...))
Success(core.Sprintf(format, args...))
}
// Error prints an error message with cross (red) to stderr and logs it.
func Error(msg string) {
LogError(msg)
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+msg))
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+msg))
}
// Errorf prints a formatted error message to stderr and logs it.
func Errorf(format string, args ...any) {
Error(fmt.Sprintf(format, args...))
Error(core.Sprintf(format, args...))
}
// ErrorWrap prints a wrapped error message to stderr and logs it.
@ -62,7 +62,7 @@ func ErrorWrap(err error, msg string) {
if err == nil {
return
}
Error(fmt.Sprintf("%s: %v", msg, err))
Error(core.Sprintf("%s: %v", msg, err))
}
// ErrorWrapVerb prints a wrapped error using i18n grammar to stderr and logs it.
@ -71,7 +71,7 @@ func ErrorWrapVerb(err error, verb, subject string) {
return
}
msg := i18n.ActionFailed(verb, subject)
Error(fmt.Sprintf("%s: %v", msg, err))
Error(core.Sprintf("%s: %v", msg, err))
}
// ErrorWrapAction prints a wrapped error using i18n grammar to stderr and logs it.
@ -80,33 +80,33 @@ func ErrorWrapAction(err error, verb string) {
return
}
msg := i18n.ActionFailed(verb, "")
Error(fmt.Sprintf("%s: %v", msg, err))
Error(core.Sprintf("%s: %v", msg, err))
}
// Warn prints a warning message with warning symbol (amber) to stderr and logs it.
func Warn(msg string) {
LogWarn(msg)
fmt.Fprintln(os.Stderr, WarningStyle.Render(Glyph(":warn:")+" "+msg))
core.Print(os.Stderr, "%s\n", WarningStyle.Render(Glyph(":warn:")+" "+msg))
}
// Warnf prints a formatted warning message to stderr and logs it.
func Warnf(format string, args ...any) {
Warn(fmt.Sprintf(format, args...))
Warn(core.Sprintf(format, args...))
}
// Info prints an info message with info symbol (blue).
func Info(msg string) {
fmt.Println(InfoStyle.Render(Glyph(":info:") + " " + msg))
core.Println(InfoStyle.Render(Glyph(":info:") + " " + msg))
}
// Infof prints a formatted info message.
func Infof(format string, args ...any) {
Info(fmt.Sprintf(format, args...))
Info(core.Sprintf(format, args...))
}
// Dim prints dimmed text.
func Dim(msg string) {
fmt.Println(DimStyle.Render(msg))
core.Println(DimStyle.Render(msg))
}
// Progress prints a progress indicator that overwrites the current line.
@ -114,25 +114,26 @@ func Dim(msg string) {
func Progress(verb string, current, total int, item ...string) {
msg := i18n.Progress(verb)
if len(item) > 0 && item[0] != "" {
fmt.Printf("\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0])
core.Print(os.Stdout, "\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0])
} else {
fmt.Printf("\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total)
core.Print(os.Stdout, "\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total)
}
}
// ProgressDone clears the progress line.
func ProgressDone() {
fmt.Print("\033[2K\r")
_, _ = io.WriteString(os.Stdout, "\033[2K\r")
}
// Label prints a "Label: value" line.
func Label(word, value string) {
fmt.Printf("%s %s\n", KeyStyle.Render(i18n.Label(word)), value)
core.Print(os.Stdout, "%s %s\n", KeyStyle.Render(i18n.Label(word)), value)
}
// Scanln reads from stdin.
func Scanln(a ...any) (int, error) {
return fmt.Scanln(a...)
// fmt.Scanln is the only way to read from stdin interactively
return scanln(a...)
}
// Task prints a task header: "[label] message"
@ -140,15 +141,15 @@ func Scanln(a ...any) (int, error) {
// cli.Task("php", "Running tests...") // [php] Running tests...
// cli.Task("go", i18n.Progress("build")) // [go] Building...
func Task(label, message string) {
fmt.Printf("%s %s\n\n", DimStyle.Render("["+label+"]"), message)
core.Print(os.Stdout, "%s %s\n\n", DimStyle.Render("["+label+"]"), message)
}
// Section prints a section header: "── SECTION ──"
//
// cli.Section("audit") // ── AUDIT ──
func Section(name string) {
header := "── " + strings.ToUpper(name) + " ──"
fmt.Println(AccentStyle.Render(header))
header := "── " + core.Upper(name) + " ──"
core.Println(AccentStyle.Render(header))
}
// Hint prints a labelled hint: "label: message"
@ -156,7 +157,7 @@ func Section(name string) {
// cli.Hint("install", "composer require vimeo/psalm")
// cli.Hint("fix", "core php fmt --fix")
func Hint(label, message string) {
fmt.Printf(" %s %s\n", DimStyle.Render(label+":"), message)
core.Print(os.Stdout, " %s %s\n", DimStyle.Render(label+":"), message)
}
// Severity prints a severity-styled message.
@ -167,7 +168,7 @@ func Hint(label, message string) {
// cli.Severity("low", "Debug enabled") // gray
func Severity(level, message string) {
var style *AnsiStyle
switch strings.ToLower(level) {
switch core.Lower(level) {
case "critical":
style = NewStyle().Bold().Foreground(ColourRed500)
case "high":
@ -179,7 +180,7 @@ func Severity(level, message string) {
default:
style = DimStyle
}
fmt.Printf(" %s %s\n", style.Render("["+level+"]"), message)
core.Print(os.Stdout, " %s %s\n", style.Render("["+level+"]"), message)
}
// Result prints a result line: "✓ message" or "✗ message"

View file

@ -2,12 +2,12 @@ package cli
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"dappco.re/go/core"
)
var stdin io.Reader = os.Stdin
@ -26,9 +26,9 @@ func newReader() *bufio.Reader {
// Prompt asks for text input with a default value.
func Prompt(label, defaultVal string) (string, error) {
if defaultVal != "" {
fmt.Printf("%s [%s]: ", label, defaultVal)
core.Print(os.Stdout, "%s [%s]: ", label, defaultVal)
} else {
fmt.Printf("%s: ", label)
core.Print(os.Stdout, "%s: ", label)
}
r := newReader()
@ -37,7 +37,7 @@ func Prompt(label, defaultVal string) (string, error) {
return "", err
}
input = strings.TrimSpace(input)
input = core.Trim(input)
if input == "" {
return defaultVal, nil
}
@ -46,11 +46,11 @@ func Prompt(label, defaultVal string) (string, error) {
// Select presents numbered options and returns the selected value.
func Select(label string, options []string) (string, error) {
fmt.Println(label)
core.Println(label)
for i, opt := range options {
fmt.Printf(" %d. %s\n", i+1, opt)
core.Print(os.Stdout, " %d. %s\n", i+1, opt)
}
fmt.Printf("Choose [1-%d]: ", len(options))
core.Print(os.Stdout, "Choose [1-%d]: ", len(options))
r := newReader()
input, err := r.ReadString('\n')
@ -58,20 +58,20 @@ func Select(label string, options []string) (string, error) {
return "", err
}
n, err := strconv.Atoi(strings.TrimSpace(input))
n, err := strconv.Atoi(core.Trim(input))
if err != nil || n < 1 || n > len(options) {
return "", errors.New("invalid selection")
return "", core.NewError("invalid selection")
}
return options[n-1], nil
}
// MultiSelect presents checkboxes (space-separated numbers).
func MultiSelect(label string, options []string) ([]string, error) {
fmt.Println(label)
core.Println(label)
for i, opt := range options {
fmt.Printf(" %d. %s\n", i+1, opt)
core.Print(os.Stdout, " %d. %s\n", i+1, opt)
}
fmt.Printf("Choose (space-separated) [1-%d]: ", len(options))
core.Print(os.Stdout, "Choose (space-separated) [1-%d]: ", len(options))
r := newReader()
input, err := r.ReadString('\n')
@ -80,7 +80,7 @@ func MultiSelect(label string, options []string) ([]string, error) {
}
var selected []string
for _, s := range strings.Fields(input) {
for s := range strings.FieldsSeq(input) {
n, err := strconv.Atoi(s)
if err != nil || n < 1 || n > len(options) {
continue

View file

@ -1,8 +1,11 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"dappco.re/go/core"
)
// RenderStyle controls how layouts are rendered.
@ -31,13 +34,13 @@ func UseRenderBoxed() { currentRenderStyle = RenderBoxed }
// Render outputs the layout to terminal.
func (c *Composite) Render() {
fmt.Print(c.String())
_, _ = io.WriteString(os.Stdout, c.String())
}
// String returns the rendered layout.
func (c *Composite) String() string {
var sb strings.Builder
c.renderTo(&sb, 0)
sb := core.NewBuilder()
c.renderTo(sb, 0)
return sb.String()
}
@ -75,7 +78,7 @@ func (c *Composite) renderSeparator(sb *strings.Builder, depth int) {
func (c *Composite) renderSlot(sb *strings.Builder, slot *Slot, depth int) {
indent := strings.Repeat(" ", depth)
for _, block := range slot.blocks {
for _, line := range strings.Split(block.Render(), "\n") {
for _, line := range core.Split(block.Render(), "\n") {
if line != "" {
sb.WriteString(indent + line + "\n")
}

View file

@ -65,9 +65,7 @@ func Init(opts Options) error {
}
// Create Core with app identity
c := core.New(core.Options{
{Key: "name", Value: opts.AppName},
})
c := core.New(core.WithOption("name", opts.AppName))
c.App().Version = opts.Version
c.App().Runtime = rootCmd

View file

@ -1,7 +1,6 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
@ -59,7 +58,7 @@ func (s *Stream) Write(text string) {
defer s.mu.Unlock()
if s.wrap <= 0 {
fmt.Fprint(s.out, text)
_, _ = io.WriteString(s.out, text)
// Track column across newlines for Done() trailing-newline logic.
if idx := strings.LastIndex(text, "\n"); idx >= 0 {
s.col = utf8.RuneCountInString(text[idx+1:])
@ -71,17 +70,17 @@ func (s *Stream) Write(text string) {
for _, r := range text {
if r == '\n' {
fmt.Fprintln(s.out)
_, _ = io.WriteString(s.out, "\n")
s.col = 0
continue
}
if s.col >= s.wrap {
fmt.Fprintln(s.out)
_, _ = io.WriteString(s.out, "\n")
s.col = 0
}
fmt.Fprint(s.out, string(r))
_, _ = io.WriteString(s.out, string(r))
s.col++
}
}
@ -107,7 +106,7 @@ func (s *Stream) WriteFrom(r io.Reader) error {
func (s *Stream) Done() {
s.mu.Lock()
if s.col > 0 {
fmt.Fprintln(s.out) // ensure trailing newline
_, _ = io.WriteString(s.out, "\n") // ensure trailing newline
}
s.mu.Unlock()
close(s.done)
@ -125,15 +124,18 @@ func (s *Stream) Column() int {
return s.col
}
// stringer matches any type with a String() method.
type stringer interface{ String() string }
// Captured returns the stream output as a string when using a bytes.Buffer.
// Panics if the output writer is not a *strings.Builder or fmt.Stringer.
// Panics if the output writer is not a *strings.Builder or Stringer.
func (s *Stream) Captured() string {
s.mu.Lock()
defer s.mu.Unlock()
if sb, ok := s.out.(*strings.Builder); ok {
return sb.String()
}
if st, ok := s.out.(fmt.Stringer); ok {
if st, ok := s.out.(stringer); ok {
return st.String()
}
return ""

View file

@ -1,15 +1,19 @@
package cli
import "fmt"
import (
"fmt"
// Sprintf formats a string (fmt.Sprintf wrapper).
"dappco.re/go/core"
)
// Sprintf formats a string (core.Sprintf wrapper).
func Sprintf(format string, args ...any) string {
return fmt.Sprintf(format, args...)
return core.Sprintf(format, args...)
}
// Sprint formats using default formats (fmt.Sprint wrapper).
// Sprint formats using default formats (core.Sprint wrapper).
func Sprint(args ...any) string {
return fmt.Sprint(args...)
return core.Sprint(args...)
}
// Styled returns text with a style applied.
@ -19,7 +23,13 @@ func Styled(style *AnsiStyle, text string) string {
// Styledf returns formatted text with a style applied.
func Styledf(style *AnsiStyle, format string, args ...any) string {
return style.Render(fmt.Sprintf(format, args...))
return style.Render(core.Sprintf(format, args...))
}
// scanln reads space-delimited tokens from stdin (fmt.Scanln wrapper).
// Kept as internal; fmt.Scanln has no core equivalent.
func scanln(a ...any) (int, error) {
return fmt.Scanln(a...)
}
// SuccessStr returns success-styled string.

View file

@ -2,9 +2,12 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"time"
"dappco.re/go/core"
)
// Tailwind colour palette (hex strings)
@ -93,15 +96,15 @@ func FormatAge(t time.Time) string {
case d < time.Minute:
return "just now"
case d < time.Hour:
return fmt.Sprintf("%dm ago", int(d.Minutes()))
return core.Sprintf("%dm ago", int(d.Minutes()))
case d < 24*time.Hour:
return fmt.Sprintf("%dh ago", int(d.Hours()))
return core.Sprintf("%dh ago", int(d.Hours()))
case d < 7*24*time.Hour:
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
return core.Sprintf("%dd ago", int(d.Hours()/24))
case d < 30*24*time.Hour:
return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7)))
return core.Sprintf("%dw ago", int(d.Hours()/(24*7)))
default:
return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30)))
return core.Sprintf("%dmo ago", int(d.Hours()/(24*30)))
}
}
@ -233,7 +236,7 @@ func (t *Table) String() string {
// Render prints the table to stdout.
func (t *Table) Render() {
fmt.Print(t.String())
_, _ = io.WriteString(os.Stdout, t.String())
}
func (t *Table) colCount() int {
@ -315,7 +318,7 @@ func (t *Table) resolveStyle(col int, value string) *AnsiStyle {
func (t *Table) renderPlain() string {
widths := t.columnWidths()
var sb strings.Builder
sb := core.NewBuilder()
sep := t.Style.Separator
if len(t.Headers) > 0 {
@ -358,7 +361,7 @@ func (t *Table) renderBordered() string {
widths := t.columnWidths()
cols := t.colCount()
var sb strings.Builder
sb := core.NewBuilder()
// Top border: ╭──────┬──────╮
sb.WriteString(b.tl)

View file

@ -1,14 +1,13 @@
package cli
import (
"fmt"
"io"
"iter"
"os"
"strings"
"sync"
"time"
"dappco.re/go/core"
"golang.org/x/term"
)
@ -171,7 +170,7 @@ func (tr *TaskTracker) waitStatic() {
if state == taskFailed {
icon = Glyph(":cross:")
}
fmt.Fprintf(tr.out, "%s %-20s %s\n", icon, name, status)
core.Print(tr.out, "%s %-20s %s\n", icon, name, status)
}
if allDone {
return
@ -203,7 +202,7 @@ func (tr *TaskTracker) waitLive() {
tr.mu.Unlock()
// Move cursor up to redraw all lines.
fmt.Fprintf(tr.out, "\033[%dA", count)
core.Print(tr.out, "\033[%dA", count)
for i := range count {
tr.renderLine(i, frame)
}
@ -244,7 +243,7 @@ func (tr *TaskTracker) renderLine(idx, frame int) {
styledStatus = DimStyle.Render(status)
}
fmt.Fprintf(tr.out, "\033[2K%s %-*s %s\n", icon, nameW, name, styledStatus)
core.Print(tr.out, "\033[2K%s %-*s %s\n", icon, nameW, name, styledStatus)
}
func (tr *TaskTracker) nameWidth() int {
@ -289,9 +288,9 @@ func (tr *TaskTracker) Summary() string {
total := len(tr.tasks)
if failed > 0 {
return fmt.Sprintf("%d/%d passed, %d failed", passed, total, failed)
return core.Sprintf("%d/%d passed, %d failed", passed, total, failed)
}
return fmt.Sprintf("%d/%d passed", passed, total)
return core.Sprintf("%d/%d passed", passed, total)
}
// String returns the current state of all tasks as plain text (no ANSI).
@ -301,7 +300,7 @@ func (tr *TaskTracker) String() string {
tr.mu.Unlock()
nameW := tr.nameWidth()
var sb strings.Builder
sb := core.NewBuilder()
for _, t := range tasks {
name, status, state := t.snapshot()
icon := "…"
@ -313,7 +312,7 @@ func (tr *TaskTracker) String() string {
case taskRunning:
icon = "⠋"
}
fmt.Fprintf(&sb, "%s %-*s %s\n", icon, nameW, name, status)
core.Print(sb, "%s %-*s %s\n", icon, nameW, name, status)
}
return sb.String()
}

View file

@ -1,9 +1,12 @@
package cli
import (
"fmt"
"io"
"iter"
"os"
"strings"
"dappco.re/go/core"
)
// TreeNode represents a node in a displayable tree structure.
@ -70,16 +73,16 @@ func (n *TreeNode) Children() iter.Seq[*TreeNode] {
// String renders the tree with box-drawing characters.
// Implements fmt.Stringer.
func (n *TreeNode) String() string {
var sb strings.Builder
sb := core.NewBuilder()
sb.WriteString(n.renderLabel())
sb.WriteByte('\n')
n.writeChildren(&sb, "")
n.writeChildren(sb, "")
return sb.String()
}
// Render prints the tree to stdout.
func (n *TreeNode) Render() {
fmt.Print(n.String())
_, _ = io.WriteString(os.Stdout, n.String())
}
func (n *TreeNode) renderLabel() string {

View file

@ -3,23 +3,26 @@ package cli
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"dappco.re/go/core"
"forge.lthn.ai/core/go-i18n"
"forge.lthn.ai/core/go-log"
)
// fmt is retained solely for fmt.Sscanf (no core equivalent).
// strings is retained for strings.FieldsSeq/SplitSeq (allowed per RFC-025).
// GhAuthenticated checks if the GitHub CLI is authenticated.
// Returns true if 'gh auth status' indicates a logged-in user.
func GhAuthenticated() bool {
cmd := exec.Command("gh", "auth", "status")
output, _ := cmd.CombinedOutput()
authenticated := strings.Contains(string(output), "Logged in")
authenticated := core.Contains(string(output), "Logged in")
if authenticated {
LogWarn("GitHub CLI authenticated", "user", log.Username())
@ -94,13 +97,13 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
// Add timeout indicator if set
if cfg.timeout > 0 {
suffix = fmt.Sprintf("%s(auto in %s) ", suffix, cfg.timeout.Round(time.Second))
suffix = core.Sprintf("%s(auto in %s) ", suffix, cfg.timeout.Round(time.Second))
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s %s", prompt, suffix)
core.Print(os.Stdout, "%s %s", prompt, suffix)
var response string
@ -114,14 +117,14 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
select {
case response = <-resultChan:
response = strings.ToLower(strings.TrimSpace(response))
response = core.Lower(core.Trim(response))
case <-time.After(cfg.timeout):
fmt.Println() // New line after timeout
core.Println() // New line after timeout
return cfg.defaultYes
}
} else {
response, _ = reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
response = core.Lower(core.Trim(response))
}
// Handle empty response
@ -142,7 +145,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
// Invalid response
if cfg.required {
fmt.Println("Please enter 'y' or 'n'")
core.Println("Please enter 'y' or 'n'")
continue
}
@ -220,18 +223,18 @@ func Question(prompt string, opts ...QuestionOption) string {
for {
// Build prompt with default
if cfg.defaultValue != "" {
fmt.Printf("%s [%s] ", prompt, cfg.defaultValue)
core.Print(os.Stdout, "%s [%s] ", prompt, cfg.defaultValue)
} else {
fmt.Printf("%s ", prompt)
core.Print(os.Stdout, "%s ", prompt)
}
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(response)
response = core.Trim(response)
// Handle empty response
if response == "" {
if cfg.required {
fmt.Println("Response required")
core.Println("Response required")
continue
}
response = cfg.defaultValue
@ -240,7 +243,7 @@ func Question(prompt string, opts ...QuestionOption) string {
// Validate if validator provided
if cfg.validator != nil {
if err := cfg.validator(response); err != nil {
fmt.Printf("Invalid: %v\n", err)
core.Print(os.Stdout, "Invalid: %v\n", err)
continue
}
}
@ -319,28 +322,28 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
}
cfg := &chooseConfig[T]{
displayFn: func(item T) string { return fmt.Sprint(item) },
displayFn: func(item T) string { return core.Sprint(item) },
}
for _, opt := range opts {
opt(cfg)
}
// Display options
fmt.Println(prompt)
core.Println(prompt)
for i, item := range items {
marker := " "
if i == cfg.defaultN {
marker = "*"
}
fmt.Printf(" %s%d. %s\n", marker, i+1, cfg.displayFn(item))
core.Print(os.Stdout, " %s%d. %s\n", marker, i+1, cfg.displayFn(item))
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("Enter number [1-%d]: ", len(items))
core.Print(os.Stdout, "Enter number [1-%d]: ", len(items))
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(response)
response = core.Trim(response)
// Empty response uses default
if response == "" {
@ -355,7 +358,7 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
}
}
fmt.Printf("Please enter a number between 1 and %d\n", len(items))
core.Print(os.Stdout, "Please enter a number between 1 and %d\n", len(items))
}
}
@ -384,24 +387,24 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
}
cfg := &chooseConfig[T]{
displayFn: func(item T) string { return fmt.Sprint(item) },
displayFn: func(item T) string { return core.Sprint(item) },
}
for _, opt := range opts {
opt(cfg)
}
// Display options
fmt.Println(prompt)
core.Println(prompt)
for i, item := range items {
fmt.Printf(" %d. %s\n", i+1, cfg.displayFn(item))
core.Print(os.Stdout, " %d. %s\n", i+1, cfg.displayFn(item))
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ")
core.Print(os.Stdout, "Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ")
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(response)
response = core.Trim(response)
// Empty response returns no selections
if response == "" {
@ -411,7 +414,7 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
// Parse the selection
selected, err := parseMultiSelection(response, len(items))
if err != nil {
fmt.Printf("Invalid selection: %v\n", err)
core.Print(os.Stdout, "Invalid selection: %v\n", err)
continue
}
@ -431,23 +434,23 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
for part := range strings.FieldsSeq(input) {
// Check for range (e.g., "1-3")
if strings.Contains(part, "-") {
if core.Contains(part, "-") {
var rangeParts []string
for p := range strings.SplitSeq(part, "-") {
rangeParts = append(rangeParts, p)
}
if len(rangeParts) != 2 {
return nil, fmt.Errorf("invalid range: %s", part)
return nil, core.NewError(core.Sprintf("invalid range: %s", part))
}
var start, end int
if _, err := fmt.Sscanf(rangeParts[0], "%d", &start); err != nil {
return nil, fmt.Errorf("invalid range start: %s", rangeParts[0])
return nil, core.NewError(core.Sprintf("invalid range start: %s", rangeParts[0]))
}
if _, err := fmt.Sscanf(rangeParts[1], "%d", &end); err != nil {
return nil, fmt.Errorf("invalid range end: %s", rangeParts[1])
return nil, core.NewError(core.Sprintf("invalid range end: %s", rangeParts[1]))
}
if start < 1 || start > maxItems || end < 1 || end > maxItems || start > end {
return nil, fmt.Errorf("range out of bounds: %s", part)
return nil, core.NewError(core.Sprintf("range out of bounds: %s", part))
}
for i := start; i <= end; i++ {
selected[i-1] = true // Convert to 0-based
@ -456,10 +459,10 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
// Single number
var n int
if _, err := fmt.Sscanf(part, "%d", &n); err != nil {
return nil, fmt.Errorf("invalid number: %s", part)
return nil, core.NewError(core.Sprintf("invalid number: %s", part))
}
if n < 1 || n > maxItems {
return nil, fmt.Errorf("number out of range: %d", n)
return nil, core.NewError(core.Sprintf("number out of range: %d", n))
}
selected[n-1] = true // Convert to 0-based
}
@ -487,22 +490,22 @@ func ChooseMultiAction[T any](verb, subject string, items []T, opts ...ChooseOpt
// Prefers 'gh repo clone' if authenticated, falls back to SSH.
func GitClone(ctx context.Context, org, repo, path string) error {
if GhAuthenticated() {
httpsURL := fmt.Sprintf("https://github.com/%s/%s.git", org, repo)
httpsURL := core.Sprintf("https://github.com/%s/%s.git", org, repo)
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", httpsURL, path)
output, err := cmd.CombinedOutput()
if err == nil {
return nil
}
errStr := strings.TrimSpace(string(output))
if strings.Contains(errStr, "already exists") {
return errors.New(errStr)
errStr := core.Trim(string(output))
if core.Contains(errStr, "already exists") {
return core.NewError(errStr)
}
}
// Fall back to SSH clone
cmd := exec.CommandContext(ctx, "git", "clone", fmt.Sprintf("git@github.com:%s/%s.git", org, repo), path)
cmd := exec.CommandContext(ctx, "git", "clone", core.Sprintf("git@github.com:%s/%s.git", org, repo), path)
output, err := cmd.CombinedOutput()
if err != nil {
return errors.New(strings.TrimSpace(string(output)))
return core.NewError(core.Trim(string(output)))
}
return nil
}