From e9e0eccd60e9ce59d12cf7f94ce6802e32319a7b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 14:31:34 +0000 Subject: [PATCH] feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports 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) --- cmd/core/config/cmd_get.go | 5 +- cmd/core/config/cmd_list.go | 5 +- cmd/core/config/cmd_path.go | 5 +- cmd/core/doctor/cmd_checks.go | 6 +- cmd/core/doctor/cmd_doctor.go | 42 +++++++------- cmd/core/doctor/cmd_environment.go | 28 +++++---- cmd/core/doctor/cmd_install.go | 21 +++---- cmd/core/help/cmd.go | 23 ++++---- cmd/core/pkgcmd/cmd_install.go | 66 ++++++++++----------- cmd/core/pkgcmd/cmd_manage.go | 92 +++++++++++++++--------------- cmd/core/pkgcmd/cmd_remove.go | 66 +++++++++++---------- cmd/core/pkgcmd/cmd_remove_test.go | 8 +-- cmd/core/pkgcmd/cmd_search.go | 56 +++++++++--------- go.mod | 2 +- go.sum | 4 +- pkg/cli/ansi.go | 12 ++-- pkg/cli/app.go | 7 +-- pkg/cli/app_test.go | 10 ++-- pkg/cli/errors.go | 31 +++++----- pkg/cli/frame.go | 7 +-- pkg/cli/layout.go | 9 +-- pkg/cli/output.go | 61 ++++++++++---------- pkg/cli/prompt.go | 28 ++++----- pkg/cli/render.go | 13 +++-- pkg/cli/runtime.go | 4 +- pkg/cli/stream.go | 18 +++--- pkg/cli/strings.go | 22 +++++-- pkg/cli/styles.go | 21 ++++--- pkg/cli/tracker.go | 17 +++--- pkg/cli/tree.go | 11 ++-- pkg/cli/utils.go | 79 +++++++++++++------------ 31 files changed, 393 insertions(+), 386 deletions(-) diff --git a/cmd/core/config/cmd_get.go b/cmd/core/config/cmd_get.go index 54aba55..b5c2bea 100644 --- a/cmd/core/config/cmd_get.go +++ b/cmd/core/config/cmd_get.go @@ -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 }) diff --git a/cmd/core/config/cmd_list.go b/cmd/core/config/cmd_list.go index 9e4f15c..5b35d1e 100644 --- a/cmd/core/config/cmd_list.go +++ b/cmd/core/config/cmd_list.go @@ -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 }) diff --git a/cmd/core/config/cmd_path.go b/cmd/core/config/cmd_path.go index d987812..b553d14 100644 --- a/cmd/core/config/cmd_path.go +++ b/cmd/core/config/cmd_path.go @@ -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 }) diff --git a/cmd/core/doctor/cmd_checks.go b/cmd/core/doctor/cmd_checks.go index 7b9047e..ccde828 100644 --- a/cmd/core/doctor/cmd_checks.go +++ b/cmd/core/doctor/cmd_checks.go @@ -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, "" } diff --git a/cmd/core/doctor/cmd_doctor.go b/cmd/core/doctor/cmd_doctor.go index a3354d7..0de85fb 100644 --- a/cmd/core/doctor/cmd_doctor.go +++ b/cmd/core/doctor/cmd_doctor.go @@ -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")) diff --git a/cmd/core/doctor/cmd_environment.go b/cmd/core/doctor/cmd_environment.go index 5190e4b..cc1b18a 100644 --- a/cmd/core/doctor/cmd_environment.go +++ b/cmd/core/doctor/cmd_environment.go @@ -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")) } } diff --git a/cmd/core/doctor/cmd_install.go b/cmd/core/doctor/cmd_install.go index 4ffb59c..4feab36 100644 --- a/cmd/core/doctor/cmd_install.go +++ b/cmd/core/doctor/cmd_install.go @@ -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")) } } diff --git a/cmd/core/help/cmd.go b/cmd/core/help/cmd.go index 67f2704..3a8a0e2 100644 --- a/cmd/core/help/cmd.go +++ b/cmd/core/help/cmd.go @@ -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() } diff --git a/cmd/core/pkgcmd/cmd_install.go b/cmd/core/pkgcmd/cmd_install.go index a486910..4ad60b0 100644 --- a/cmd/core/pkgcmd/cmd_install.go +++ b/cmd/core/pkgcmd/cmd_install.go @@ -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" diff --git a/cmd/core/pkgcmd/cmd_manage.go b/cmd/core/pkgcmd/cmd_manage.go index 2964d3f..a12161a 100644 --- a/cmd/core/pkgcmd/cmd_manage.go +++ b/cmd/core/pkgcmd/cmd_manage.go @@ -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 diff --git a/cmd/core/pkgcmd/cmd_remove.go b/cmd/core/pkgcmd/cmd_remove.go index ba3fa58..1317618 100644 --- a/cmd/core/pkgcmd/cmd_remove.go +++ b/cmd/core/pkgcmd/cmd_remove.go @@ -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 diff --git a/cmd/core/pkgcmd/cmd_remove_test.go b/cmd/core/pkgcmd/cmd_remove_test.go index 442a08e..9a577a2 100644 --- a/cmd/core/pkgcmd/cmd_remove_test.go +++ b/cmd/core/pkgcmd/cmd_remove_test.go @@ -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()) diff --git a/cmd/core/pkgcmd/cmd_search.go b/cmd/core/pkgcmd/cmd_search.go index 615a2d6..110bf4f 100644 --- a/cmd/core/pkgcmd/cmd_search.go +++ b/cmd/core/pkgcmd/cmd_search.go @@ -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/", org))) + core.Println() + core.Print(os.Stdout, "%s %s\n", i18n.T("common.hint.install_with"), dimStyle.Render(core.Sprintf("core pkg install %s/", 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 diff --git a/go.mod b/go.mod index 9ab2521..4cfd88a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b3913d6..0c413c2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/cli/ansi.go b/pkg/cli/ansi.go index e4df66e..06df3cf 100644 --- a/pkg/cli/ansi.go +++ b/pkg/cli/ansi.go @@ -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 } diff --git a/pkg/cli/app.go b/pkg/cli/app.go index fbc96c6..7c64df3 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -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: diff --git a/pkg/cli/app_test.go b/pkg/cli/app_test.go index c11d5fe..831fd91 100644 --- a/pkg/cli/app_test.go +++ b/pkg/cli/app_test.go @@ -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))) } }() diff --git a/pkg/cli/errors.go b/pkg/cli/errors.go index f3fc105..8acadcb 100644 --- a/pkg/cli/errors.go +++ b/pkg/cli/errors.go @@ -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) } diff --git a/pkg/cli/frame.go b/pkg/cli/frame.go index 82e8108..a3ac5fa 100644 --- a/pkg/cli/frame.go +++ b/pkg/cli/frame.go @@ -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 diff --git a/pkg/cli/layout.go b/pkg/cli/layout.go index e0acb48..4bd7148 100644 --- a/pkg/cli/layout.go +++ b/pkg/cli/layout.go @@ -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)) } } diff --git a/pkg/cli/output.go b/pkg/cli/output.go index 5670922..4d564ec 100644 --- a/pkg/cli/output.go +++ b/pkg/cli/output.go @@ -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" diff --git a/pkg/cli/prompt.go b/pkg/cli/prompt.go index 09a383c..38151ff 100644 --- a/pkg/cli/prompt.go +++ b/pkg/cli/prompt.go @@ -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 diff --git a/pkg/cli/render.go b/pkg/cli/render.go index 95bb05c..d9f9677 100644 --- a/pkg/cli/render.go +++ b/pkg/cli/render.go @@ -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") } diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index 17cb6f0..abb4fb9 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -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 diff --git a/pkg/cli/stream.go b/pkg/cli/stream.go index e12aa4b..f318127 100644 --- a/pkg/cli/stream.go +++ b/pkg/cli/stream.go @@ -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 "" diff --git a/pkg/cli/strings.go b/pkg/cli/strings.go index 1e587ad..5c0e991 100644 --- a/pkg/cli/strings.go +++ b/pkg/cli/strings.go @@ -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. diff --git a/pkg/cli/styles.go b/pkg/cli/styles.go index 3813b1a..f09d0c7 100644 --- a/pkg/cli/styles.go +++ b/pkg/cli/styles.go @@ -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) diff --git a/pkg/cli/tracker.go b/pkg/cli/tracker.go index c64c2e7..b5d482e 100644 --- a/pkg/cli/tracker.go +++ b/pkg/cli/tracker.go @@ -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() } diff --git a/pkg/cli/tree.go b/pkg/cli/tree.go index ead9195..8a58c1d 100644 --- a/pkg/cli/tree.go +++ b/pkg/cli/tree.go @@ -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 { diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index 8a33b27..95fa093 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -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 }