feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
All checks were successful
Security Scan / security (push) Successful in 13s
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:
parent
e177418c90
commit
e9e0eccd60
31 changed files with 393 additions and 386 deletions
|
|
@ -1,8 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"dappco.re/go/core"
|
||||||
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,7 +19,7 @@ func addGetCommand(parent *cli.Command) {
|
||||||
return cli.Err("key not found: %s", key)
|
return cli.Err("key not found: %s", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(value)
|
core.Println(value)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"maps"
|
"maps"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -26,7 +27,7 @@ func addListCommand(parent *cli.Command) {
|
||||||
return cli.Wrap(err, "failed to format config")
|
return cli.Wrap(err, "failed to format config")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print(string(out))
|
core.Print(os.Stdout, "%s", string(out))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"dappco.re/go/core"
|
||||||
|
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -13,7 +12,7 @@ func addPathCommand(parent *cli.Command) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(cfg.Path())
|
core.Println(cfg.Path())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ package doctor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -93,9 +93,9 @@ func runCheck(c check) (bool, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract first line as version
|
// 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 {
|
if len(lines) > 0 {
|
||||||
return true, strings.TrimSpace(lines[0])
|
return true, core.Trim(lines[0])
|
||||||
}
|
}
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
package doctor
|
package doctor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"os"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -32,72 +32,72 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor(verbose bool) error {
|
func runDoctor(verbose bool) error {
|
||||||
fmt.Println(i18n.T("common.progress.checking", map[string]any{"Item": "development environment"}))
|
core.Println(i18n.T("common.progress.checking", map[string]any{"Item": "development environment"}))
|
||||||
fmt.Println()
|
core.Println()
|
||||||
|
|
||||||
var passed, failed, optional int
|
var passed, failed, optional int
|
||||||
|
|
||||||
// Check required tools
|
// Check required tools
|
||||||
fmt.Println(i18n.T("cmd.doctor.required"))
|
core.Println(i18n.T("cmd.doctor.required"))
|
||||||
for _, c := range requiredChecks() {
|
for _, c := range requiredChecks() {
|
||||||
ok, version := runCheck(c)
|
ok, version := runCheck(c)
|
||||||
if ok {
|
if ok {
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println(formatCheckResult(true, c.name, version))
|
core.Println(formatCheckResult(true, c.name, version))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(formatCheckResult(true, c.name, ""))
|
core.Println(formatCheckResult(true, c.name, ""))
|
||||||
}
|
}
|
||||||
passed++
|
passed++
|
||||||
} else {
|
} 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++
|
failed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check optional tools
|
// 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() {
|
for _, c := range optionalChecks() {
|
||||||
ok, version := runCheck(c)
|
ok, version := runCheck(c)
|
||||||
if ok {
|
if ok {
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Println(formatCheckResult(true, c.name, version))
|
core.Println(formatCheckResult(true, c.name, version))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(formatCheckResult(true, c.name, ""))
|
core.Println(formatCheckResult(true, c.name, ""))
|
||||||
}
|
}
|
||||||
passed++
|
passed++
|
||||||
} else {
|
} 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++
|
optional++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check GitHub access
|
// 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() {
|
if checkGitHubSSH() {
|
||||||
fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
|
core.Println(formatCheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
|
||||||
} else {
|
} 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++
|
failed++
|
||||||
}
|
}
|
||||||
|
|
||||||
if checkGitHubCLI() {
|
if checkGitHubCLI() {
|
||||||
fmt.Println(formatCheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
|
core.Println(formatCheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
|
||||||
} else {
|
} 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++
|
failed++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check workspace
|
// 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()
|
checkWorkspace()
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
core.Println()
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
cli.Error(i18n.T("cmd.doctor.issues", map[string]any{"Count": failed}))
|
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()
|
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"))
|
cli.Success(i18n.T("cmd.doctor.ready"))
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package doctor
|
package doctor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
"forge.lthn.ai/core/go-io"
|
"forge.lthn.ai/core/go-io"
|
||||||
"forge.lthn.ai/core/go-scm/repos"
|
"forge.lthn.ai/core/go-scm/repos"
|
||||||
|
|
@ -21,11 +19,11 @@ func checkGitHubSSH() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
sshDir := filepath.Join(home, ".ssh")
|
sshDir := core.JoinPath(home, ".ssh")
|
||||||
keyPatterns := []string{"id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"}
|
keyPatterns := []string{"id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"}
|
||||||
|
|
||||||
for _, key := range keyPatterns {
|
for _, key := range keyPatterns {
|
||||||
keyPath := filepath.Join(sshDir, key)
|
keyPath := core.JoinPath(sshDir, key)
|
||||||
if _, err := os.Stat(keyPath); err == nil {
|
if _, err := os.Stat(keyPath); err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -39,14 +37,14 @@ func checkGitHubCLI() bool {
|
||||||
cmd := exec.Command("gh", "auth", "status")
|
cmd := exec.Command("gh", "auth", "status")
|
||||||
output, _ := cmd.CombinedOutput()
|
output, _ := cmd.CombinedOutput()
|
||||||
// Check for any successful login (even if there's also a failing token)
|
// 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
|
// checkWorkspace checks for repos.yaml and counts cloned repos
|
||||||
func checkWorkspace() {
|
func checkWorkspace() {
|
||||||
registryPath, err := repos.FindRegistry(io.Local)
|
registryPath, err := repos.FindRegistry(io.Local)
|
||||||
if err == nil {
|
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)
|
reg, err := repos.LoadRegistry(io.Local, registryPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -54,26 +52,26 @@ func checkWorkspace() {
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "./packages"
|
basePath = "./packages"
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(basePath) {
|
if !core.PathIsAbs(basePath) {
|
||||||
basePath = filepath.Join(filepath.Dir(registryPath), basePath)
|
basePath = core.JoinPath(core.PathDir(registryPath), basePath)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(basePath, "~/") {
|
if core.HasPrefix(basePath, "~/") {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
basePath = filepath.Join(home, basePath[2:])
|
basePath = core.JoinPath(home, basePath[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count existing repos
|
// Count existing repos
|
||||||
allRepos := reg.List()
|
allRepos := reg.List()
|
||||||
var cloned int
|
var cloned int
|
||||||
for _, repo := range allRepos {
|
for _, repo := range allRepos {
|
||||||
repoPath := filepath.Join(basePath, repo.Name)
|
repoPath := core.JoinPath(basePath, repo.Name)
|
||||||
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
|
if _, err := os.Stat(core.JoinPath(repoPath, ".git")); err == nil {
|
||||||
cloned++
|
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 {
|
} 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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package doctor
|
package doctor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -11,16 +12,16 @@ import (
|
||||||
func printInstallInstructions() {
|
func printInstallInstructions() {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos"))
|
core.Print(os.Stdout, " %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_cask"))
|
||||||
case "linux":
|
case "linux":
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_header"))
|
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_header"))
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_git"))
|
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_git"))
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_gh"))
|
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_gh"))
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_php"))
|
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_linux_php"))
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_node"))
|
core.Print(os.Stdout, " %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_pnpm"))
|
||||||
default:
|
default:
|
||||||
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_other"))
|
core.Print(os.Stdout, " %s\n", i18n.T("cmd.doctor.install_other"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package help
|
package help
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/cli/pkg/cli"
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||||||
"forge.lthn.ai/core/go-help"
|
"forge.lthn.ai/core/go-help"
|
||||||
)
|
)
|
||||||
|
|
@ -19,28 +20,28 @@ func AddHelpCommands(root *cli.Command) {
|
||||||
if searchFlag != "" {
|
if searchFlag != "" {
|
||||||
results := catalog.Search(searchFlag)
|
results := catalog.Search(searchFlag)
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
fmt.Println("No topics found.")
|
core.Println("No topics found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("Search Results:")
|
core.Println("Search Results:")
|
||||||
for _, res := range 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
topics := catalog.List()
|
topics := catalog.List()
|
||||||
fmt.Println("Available Help Topics:")
|
core.Println("Available Help Topics:")
|
||||||
for _, t := range 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topic, err := catalog.Get(args[0])
|
topic, err := catalog.Get(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
core.Print(os.Stdout, "Error: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,8 +56,8 @@ func AddHelpCommands(root *cli.Command) {
|
||||||
func renderTopic(t *help.Topic) {
|
func renderTopic(t *help.Topic) {
|
||||||
// Simple ANSI rendering for now
|
// Simple ANSI rendering for now
|
||||||
// Use explicit ANSI codes or just print
|
// Use explicit ANSI codes or just print
|
||||||
fmt.Printf("\n\033[1;34m%s\033[0m\n", t.Title) // Blue bold title
|
core.Print(os.Stdout, "\n\033[1;34m%s\033[0m\n", t.Title) // Blue bold title
|
||||||
fmt.Println("----------------------------------------")
|
core.Println("----------------------------------------")
|
||||||
fmt.Println(t.Content)
|
core.Println(t.Content)
|
||||||
fmt.Println()
|
core.Println()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,15 @@ package pkgcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
coreio "forge.lthn.ai/core/go-io"
|
coreio "forge.lthn.ai/core/go-io"
|
||||||
"forge.lthn.ai/core/go-scm/repos"
|
"forge.lthn.ai/core/go-scm/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
installTargetDir string
|
installTargetDir string
|
||||||
installAddToReg bool
|
installAddToReg bool
|
||||||
|
|
@ -30,7 +24,7 @@ func addPkgInstallCommand(parent *cobra.Command) {
|
||||||
Long: i18n.T("cmd.pkg.install.long"),
|
Long: i18n.T("cmd.pkg.install.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return 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)
|
return runPkgInstall(args[0], installTargetDir, installAddToReg)
|
||||||
},
|
},
|
||||||
|
|
@ -46,9 +40,9 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Parse org/repo
|
// Parse org/repo
|
||||||
parts := strings.Split(repoArg, "/")
|
parts := core.Split(repoArg, "/")
|
||||||
if len(parts) != 2 {
|
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]
|
org, repoName := parts[0], parts[1]
|
||||||
|
|
||||||
|
|
@ -60,8 +54,8 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
if targetDir == "" {
|
if targetDir == "" {
|
||||||
targetDir = "./packages"
|
targetDir = "./packages"
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(targetDir) {
|
if !core.PathIsAbs(targetDir) {
|
||||||
targetDir = filepath.Join(filepath.Dir(regPath), 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()
|
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")) {
|
if coreio.Local.Exists(core.JoinPath(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}))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := coreio.Local.EnsureDir(targetDir); err != 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)
|
core.Print(os.Stdout, "%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)
|
core.Print(os.Stdout, "%s %s\n", dimStyle.Render(i18n.Label("target")), repoPath)
|
||||||
fmt.Println()
|
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)
|
err := gitClone(ctx, org, repoName, repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
core.Print(os.Stdout, "%s\n", errorStyle.Render("✗ "+err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("%s\n", successStyle.Render("✓"))
|
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
|
||||||
|
|
||||||
if addToRegistry {
|
if addToRegistry {
|
||||||
if err := addToRegistryFile(org, repoName); err != nil {
|
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 {
|
} 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()
|
core.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.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +109,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
func addToRegistryFile(org, repoName string) error {
|
func addToRegistryFile(org, repoName string) error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
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)
|
reg, err := repos.LoadRegistry(coreio.Local, regPath)
|
||||||
|
|
@ -133,7 +127,7 @@ func addToRegistryFile(org, repoName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
repoType := detectRepoType(repoName)
|
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)
|
repoName, repoType)
|
||||||
|
|
||||||
content += entry
|
content += entry
|
||||||
|
|
@ -141,20 +135,20 @@ func addToRegistryFile(org, repoName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectRepoType(name string) string {
|
func detectRepoType(name string) string {
|
||||||
lower := strings.ToLower(name)
|
lower := core.Lower(name)
|
||||||
if strings.Contains(lower, "-mod-") || strings.HasSuffix(lower, "-mod") {
|
if core.Contains(lower, "-mod-") || core.HasSuffix(lower, "-mod") {
|
||||||
return "module"
|
return "module"
|
||||||
}
|
}
|
||||||
if strings.Contains(lower, "-plug-") || strings.HasSuffix(lower, "-plug") {
|
if core.Contains(lower, "-plug-") || core.HasSuffix(lower, "-plug") {
|
||||||
return "plugin"
|
return "plugin"
|
||||||
}
|
}
|
||||||
if strings.Contains(lower, "-services-") || strings.HasSuffix(lower, "-services") {
|
if core.Contains(lower, "-services-") || core.HasSuffix(lower, "-services") {
|
||||||
return "service"
|
return "service"
|
||||||
}
|
}
|
||||||
if strings.Contains(lower, "-website-") || strings.HasSuffix(lower, "-website") {
|
if core.Contains(lower, "-website-") || core.HasSuffix(lower, "-website") {
|
||||||
return "website"
|
return "website"
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(lower, "core-") {
|
if core.HasPrefix(lower, "core-") {
|
||||||
return "package"
|
return "package"
|
||||||
}
|
}
|
||||||
return "package"
|
return "package"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package pkgcmd
|
package pkgcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"os"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
coreio "forge.lthn.ai/core/go-io"
|
coreio "forge.lthn.ai/core/go-io"
|
||||||
"forge.lthn.ai/core/go-scm/repos"
|
"forge.lthn.ai/core/go-scm/repos"
|
||||||
|
|
@ -30,34 +28,34 @@ func addPkgListCommand(parent *cobra.Command) {
|
||||||
func runPkgList() error {
|
func runPkgList() error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
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)
|
reg, err := repos.LoadRegistry(coreio.Local, regPath)
|
||||||
if err != nil {
|
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
|
basePath := reg.BasePath
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "."
|
basePath = "."
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(basePath) {
|
if !core.PathIsAbs(basePath) {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
basePath = core.JoinPath(core.PathDir(regPath), basePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
allRepos := reg.List()
|
allRepos := reg.List()
|
||||||
if len(allRepos) == 0 {
|
if len(allRepos) == 0 {
|
||||||
fmt.Println(i18n.T("cmd.pkg.list.no_packages"))
|
core.Println(i18n.T("cmd.pkg.list.no_packages"))
|
||||||
return nil
|
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
|
var installed, missing int
|
||||||
for _, r := range allRepos {
|
for _, r := range allRepos {
|
||||||
repoPath := filepath.Join(basePath, r.Name)
|
repoPath := core.JoinPath(basePath, r.Name)
|
||||||
exists := coreio.Local.Exists(filepath.Join(repoPath, ".git"))
|
exists := coreio.Local.Exists(core.JoinPath(repoPath, ".git"))
|
||||||
if exists {
|
if exists {
|
||||||
installed++
|
installed++
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -77,15 +75,15 @@ func runPkgList() error {
|
||||||
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
|
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name))
|
core.Print(os.Stdout, " %s %s\n", status, repoNameStyle.Render(r.Name))
|
||||||
fmt.Printf(" %s\n", desc)
|
core.Print(os.Stdout, " %s\n", desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
core.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.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 {
|
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
|
return nil
|
||||||
|
|
@ -101,7 +99,7 @@ func addPkgUpdateCommand(parent *cobra.Command) {
|
||||||
Long: i18n.T("cmd.pkg.update.long"),
|
Long: i18n.T("cmd.pkg.update.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if !updateAll && len(args) == 0 {
|
if !updateAll && len(args) == 0 {
|
||||||
return errors.New(i18n.T("cmd.pkg.error.specify_package"))
|
return core.NewError(i18n.T("cmd.pkg.error.specify_package"))
|
||||||
}
|
}
|
||||||
return runPkgUpdate(args, updateAll)
|
return runPkgUpdate(args, updateAll)
|
||||||
},
|
},
|
||||||
|
|
@ -115,20 +113,20 @@ func addPkgUpdateCommand(parent *cobra.Command) {
|
||||||
func runPkgUpdate(packages []string, all bool) error {
|
func runPkgUpdate(packages []string, all bool) error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
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)
|
reg, err := repos.LoadRegistry(coreio.Local, regPath)
|
||||||
if err != nil {
|
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
|
basePath := reg.BasePath
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "."
|
basePath = "."
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(basePath) {
|
if !core.PathIsAbs(basePath) {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
basePath = core.JoinPath(core.PathDir(regPath), basePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toUpdate []string
|
var toUpdate []string
|
||||||
|
|
@ -140,39 +138,39 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
toUpdate = packages
|
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
|
var updated, skipped, failed int
|
||||||
for _, name := range toUpdate {
|
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 {
|
if _, err := coreio.Local.List(core.JoinPath(repoPath, ".git")); err != nil {
|
||||||
fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
|
core.Print(os.Stdout, " %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
|
||||||
skipped++
|
skipped++
|
||||||
continue
|
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")
|
cmd := exec.Command("git", "-C", repoPath, "pull", "--ff-only")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s\n", errorStyle.Render("✗"))
|
core.Print(os.Stdout, "%s\n", errorStyle.Render("✗"))
|
||||||
fmt.Printf(" %s\n", strings.TrimSpace(string(output)))
|
core.Print(os.Stdout, " %s\n", core.Trim(string(output)))
|
||||||
failed++
|
failed++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(string(output), "Already up to date") {
|
if core.Contains(string(output), "Already up to date") {
|
||||||
fmt.Printf("%s\n", dimStyle.Render(i18n.T("common.status.up_to_date")))
|
core.Print(os.Stdout, "%s\n", dimStyle.Render(i18n.T("common.status.up_to_date")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s\n", successStyle.Render("✓"))
|
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
|
||||||
}
|
}
|
||||||
updated++
|
updated++
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
core.Println()
|
||||||
fmt.Printf("%s %s\n",
|
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}))
|
dimStyle.Render(i18n.T("i18n.done.update")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated, "Skipped": skipped, "Failed": failed}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -195,30 +193,30 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
|
||||||
func runPkgOutdated() error {
|
func runPkgOutdated() error {
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
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)
|
reg, err := repos.LoadRegistry(coreio.Local, regPath)
|
||||||
if err != nil {
|
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
|
basePath := reg.BasePath
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "."
|
basePath = "."
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(basePath) {
|
if !core.PathIsAbs(basePath) {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), 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
|
var outdated, upToDate, notInstalled int
|
||||||
|
|
||||||
for _, r := range reg.List() {
|
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++
|
notInstalled++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -233,9 +231,9 @@ func runPkgOutdated() error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
count := strings.TrimSpace(string(output))
|
count := core.Trim(string(output))
|
||||||
if count != "0" {
|
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}))
|
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
|
||||||
outdated++
|
outdated++
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -243,13 +241,13 @@ func runPkgOutdated() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
core.Println()
|
||||||
if outdated == 0 {
|
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 {
|
} 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}))
|
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
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,10 @@
|
||||||
package pkgcmd
|
package pkgcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"os"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
coreio "forge.lthn.ai/core/go-io"
|
coreio "forge.lthn.ai/core/go-io"
|
||||||
"forge.lthn.ai/core/go-scm/repos"
|
"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.`,
|
changes or unpushed branches. Use --force to skip safety checks.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return errors.New(i18n.T("cmd.pkg.error.repo_required"))
|
return core.NewError(i18n.T("cmd.pkg.error.repo_required"))
|
||||||
}
|
}
|
||||||
return runPkgRemove(args[0], removeForce)
|
return runPkgRemove(args[0], removeForce)
|
||||||
},
|
},
|
||||||
|
|
@ -45,49 +43,49 @@ func runPkgRemove(name string, force bool) error {
|
||||||
// Find package path via registry
|
// Find package path via registry
|
||||||
regPath, err := repos.FindRegistry(coreio.Local)
|
regPath, err := repos.FindRegistry(coreio.Local)
|
||||||
if err != nil {
|
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)
|
reg, err := repos.LoadRegistry(coreio.Local, regPath)
|
||||||
if err != nil {
|
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
|
basePath := reg.BasePath
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "."
|
basePath = "."
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(basePath) {
|
if !core.PathIsAbs(basePath) {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), 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")) {
|
if !coreio.Local.IsDir(core.JoinPath(repoPath, ".git")) {
|
||||||
return fmt.Errorf("package %s is not installed at %s", name, repoPath)
|
return core.NewError(core.Sprintf("package %s is not installed at %s", name, repoPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
blocked, reasons := checkRepoSafety(repoPath)
|
blocked, reasons := checkRepoSafety(repoPath)
|
||||||
if blocked {
|
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 {
|
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")
|
core.Print(os.Stdout, "\nResolve the issues above or use --force to override.\n")
|
||||||
return errors.New("package has unresolved changes")
|
return core.NewError("package has unresolved changes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the directory
|
// 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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", successStyle.Render("ok"))
|
core.Print(os.Stdout, "%s\n", successStyle.Render("ok"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,48 +94,48 @@ func checkRepoSafety(repoPath string) (blocked bool, reasons []string) {
|
||||||
// Check for uncommitted changes (staged, unstaged, untracked)
|
// Check for uncommitted changes (staged, unstaged, untracked)
|
||||||
cmd := exec.Command("git", "-C", repoPath, "status", "--porcelain")
|
cmd := exec.Command("git", "-C", repoPath, "status", "--porcelain")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err == nil && strings.TrimSpace(string(output)) != "" {
|
if err == nil && core.Trim(string(output)) != "" {
|
||||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
lines := core.Split(core.Trim(string(output)), "\n")
|
||||||
blocked = true
|
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
|
// Check for unpushed commits on current branch
|
||||||
cmd = exec.Command("git", "-C", repoPath, "log", "--oneline", "@{u}..HEAD")
|
cmd = exec.Command("git", "-C", repoPath, "log", "--oneline", "@{u}..HEAD")
|
||||||
output, err = cmd.Output()
|
output, err = cmd.Output()
|
||||||
if err == nil && strings.TrimSpace(string(output)) != "" {
|
if err == nil && core.Trim(string(output)) != "" {
|
||||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
lines := core.Split(core.Trim(string(output)), "\n")
|
||||||
blocked = true
|
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
|
// Check all local branches for unpushed work
|
||||||
cmd = exec.Command("git", "-C", repoPath, "branch", "--no-merged", "origin/HEAD")
|
cmd = exec.Command("git", "-C", repoPath, "branch", "--no-merged", "origin/HEAD")
|
||||||
output, _ = cmd.Output()
|
output, _ = cmd.Output()
|
||||||
if trimmed := strings.TrimSpace(string(output)); trimmed != "" {
|
if trimmed := core.Trim(string(output)); trimmed != "" {
|
||||||
branches := strings.Split(trimmed, "\n")
|
branches := core.Split(trimmed, "\n")
|
||||||
var unmerged []string
|
var unmerged []string
|
||||||
for _, b := range branches {
|
for _, b := range branches {
|
||||||
b = strings.TrimSpace(b)
|
b = core.Trim(b)
|
||||||
b = strings.TrimPrefix(b, "* ")
|
b = core.TrimPrefix(b, "* ")
|
||||||
if b != "" {
|
if b != "" {
|
||||||
unmerged = append(unmerged, b)
|
unmerged = append(unmerged, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(unmerged) > 0 {
|
if len(unmerged) > 0 {
|
||||||
blocked = true
|
blocked = true
|
||||||
reasons = append(reasons, fmt.Sprintf("has %d unmerged branches: %s",
|
reasons = append(reasons, core.Sprintf("has %d unmerged branches: %s",
|
||||||
len(unmerged), strings.Join(unmerged, ", ")))
|
len(unmerged), core.Join(", ", unmerged...)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for stashed changes
|
// Check for stashed changes
|
||||||
cmd = exec.Command("git", "-C", repoPath, "stash", "list")
|
cmd = exec.Command("git", "-C", repoPath, "stash", "list")
|
||||||
output, err = cmd.Output()
|
output, err = cmd.Output()
|
||||||
if err == nil && strings.TrimSpace(string(output)) != "" {
|
if err == nil && core.Trim(string(output)) != "" {
|
||||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
lines := core.Split(core.Trim(string(output)), "\n")
|
||||||
blocked = true
|
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
|
return blocked, reasons
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ package pkgcmd
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestRepo(t *testing.T, dir, name string) string {
|
func setupTestRepo(t *testing.T, dir, name string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
repoPath := filepath.Join(dir, name)
|
repoPath := core.JoinPath(dir, name)
|
||||||
require.NoError(t, os.MkdirAll(repoPath, 0755))
|
require.NoError(t, os.MkdirAll(repoPath, 0755))
|
||||||
|
|
||||||
cmds := [][]string{
|
cmds := [][]string{
|
||||||
|
|
@ -43,7 +43,7 @@ func TestCheckRepoSafety_UncommittedChanges(t *testing.T) {
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
repoPath := setupTestRepo(t, tmp, "dirty-repo")
|
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)
|
blocked, reasons := checkRepoSafety(repoPath)
|
||||||
assert.True(t, blocked)
|
assert.True(t, blocked)
|
||||||
|
|
@ -56,7 +56,7 @@ func TestCheckRepoSafety_Stash(t *testing.T) {
|
||||||
repoPath := setupTestRepo(t, tmp, "stash-repo")
|
repoPath := setupTestRepo(t, tmp, "stash-repo")
|
||||||
|
|
||||||
// Create a file, add, stash
|
// 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 := exec.Command("git", "add", ".")
|
||||||
cmd.Dir = repoPath
|
cmd.Dir = repoPath
|
||||||
require.NoError(t, cmd.Run())
|
require.NoError(t, cmd.Run())
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,13 @@ package pkgcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-cache"
|
"forge.lthn.ai/core/go-cache"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
coreio "forge.lthn.ai/core/go-io"
|
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
|
// Initialize cache in workspace .core/ directory
|
||||||
var cacheDir string
|
var cacheDir string
|
||||||
if regPath, err := repos.FindRegistry(coreio.Local); err == nil {
|
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)
|
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 {
|
if found, err := c.Get(cacheKey, &ghRepos); found && err == nil {
|
||||||
fromCache = true
|
fromCache = true
|
||||||
age := c.Age(cacheKey)
|
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
|
// Fetch from GitHub if not cached
|
||||||
if !fromCache {
|
if !fromCache {
|
||||||
if !ghAuthenticated() {
|
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") != "" {
|
if os.Getenv("GH_TOKEN") != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("note")), i18n.T("cmd.pkg.search.gh_token_warning"))
|
core.Print(os.Stdout, "%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\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,
|
cmd := exec.Command("gh", "repo", "list", org,
|
||||||
"--json", "name,description,visibility,updatedAt,primaryLanguage",
|
"--json", "name,description,visibility,updatedAt,primaryLanguage",
|
||||||
"--limit", fmt.Sprintf("%d", limit))
|
"--limit", core.Sprintf("%d", limit))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println()
|
core.Println()
|
||||||
errStr := strings.TrimSpace(string(output))
|
errStr := core.Trim(string(output))
|
||||||
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
|
if core.Contains(errStr, "401") || core.Contains(errStr, "Bad credentials") {
|
||||||
return errors.New(i18n.T("cmd.pkg.error.auth_failed"))
|
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 {
|
r := core.JSONUnmarshal(output, &ghRepos)
|
||||||
return fmt.Errorf("%s: %w", i18n.T("i18n.fail.parse", "results"), err)
|
if !r.OK {
|
||||||
|
return core.NewError(core.Sprintf("%s: %v", i18n.T("i18n.fail.parse", "results"), r.Value))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
_ = c.Set(cacheKey, ghRepos)
|
_ = c.Set(cacheKey, ghRepos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", successStyle.Render("✓"))
|
core.Print(os.Stdout, "%s\n", successStyle.Render("✓"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by glob pattern and type
|
// 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) {
|
if !matchGlob(pattern, r.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if repoType != "" && !strings.Contains(r.Name, repoType) {
|
if repoType != "" && !core.Contains(r.Name, repoType) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
filtered = append(filtered, r)
|
filtered = append(filtered, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filtered) == 0 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +150,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
return cmp.Compare(a.Name, b.Name)
|
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 {
|
for _, r := range filtered {
|
||||||
visibility := ""
|
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"))
|
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility)
|
core.Print(os.Stdout, " %s%s\n", repoNameStyle.Render(r.Name), visibility)
|
||||||
fmt.Printf(" %s\n", desc)
|
core.Print(os.Stdout, " %s\n", desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
core.Println()
|
||||||
fmt.Printf("%s %s\n", i18n.T("common.hint.install_with"), dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org)))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +182,7 @@ func matchGlob(pattern, name string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(pattern, "*")
|
parts := core.Split(pattern, "*")
|
||||||
pos := 0
|
pos := 0
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if part == "" {
|
if part == "" {
|
||||||
|
|
@ -194,12 +192,12 @@ func matchGlob(pattern, name string) bool {
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if i == 0 && !strings.HasPrefix(pattern, "*") && idx != 0 {
|
if i == 0 && !core.HasPrefix(pattern, "*") && idx != 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
pos += idx + len(part)
|
pos += idx + len(part)
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(pattern, "*") && pos != len(name) {
|
if !core.HasSuffix(pattern, "*") && pos != len(name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -2,7 +2,7 @@ module forge.lthn.ai/core/cli
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require dappco.re/go/core v0.4.7
|
require dappco.re/go/core v0.8.0-alpha.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
forge.lthn.ai/core/go-i18n v0.1.7
|
forge.lthn.ai/core/go-i18n v0.1.7
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,5 +1,5 @@
|
||||||
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
|
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||||
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
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 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
|
||||||
forge.lthn.ai/core/go v0.3.2/go.mod h1:f7/zb3Labn4ARfwTq5Bi2AFHY+uxyPHozO+hLb54eFo=
|
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=
|
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ANSI escape codes
|
// ANSI escape codes
|
||||||
|
|
@ -134,24 +134,24 @@ func (s *AnsiStyle) Render(text string) string {
|
||||||
return text
|
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.
|
// fgColorHex converts a hex string to an ANSI foreground color code.
|
||||||
func fgColorHex(hex string) string {
|
func fgColorHex(hex string) string {
|
||||||
r, g, b := hexToRGB(hex)
|
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.
|
// bgColorHex converts a hex string to an ANSI background color code.
|
||||||
func bgColorHex(hex string) string {
|
func bgColorHex(hex string) string {
|
||||||
r, g, b := hexToRGB(hex)
|
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.
|
// hexToRGB converts a hex string to RGB values.
|
||||||
func hexToRGB(hex string) (int, int, int) {
|
func hexToRGB(hex string) (int, int, int) {
|
||||||
hex = strings.TrimPrefix(hex, "#")
|
hex = core.TrimPrefix(hex, "#")
|
||||||
if len(hex) != 6 {
|
if len(hex) != 6 {
|
||||||
return 255, 255, 255
|
return 255, 255, 255
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,13 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
"forge.lthn.ai/core/go-log"
|
"forge.lthn.ai/core/go-log"
|
||||||
"dappco.re/go/core"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -83,7 +82,7 @@ func MainWithLocales(locales []LocaleSource, commands ...CommandSetup) {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
log.Error("recovered from panic", "error", r, "stack", string(debug.Stack()))
|
log.Error("recovered from panic", "error", r, "stack", string(debug.Stack()))
|
||||||
Shutdown()
|
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{
|
return &cobra.Command{
|
||||||
Use: "completion [bash|zsh|fish|powershell]",
|
Use: "completion [bash|zsh|fish|powershell]",
|
||||||
Short: "Generate shell completion script",
|
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:
|
To load completions:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"github.com/stretchr/testify/assert"
|
"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)
|
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)
|
}(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ func TestMainPanicRecoveryPattern(t *testing.T) {
|
||||||
|
|
||||||
// Mock implementations
|
// Mock implementations
|
||||||
mockLogError := func(msg string, args ...any) {
|
mockLogError := func(msg string, args ...any) {
|
||||||
fmt.Fprintf(&logBuffer, msg, args...)
|
core.Print(&logBuffer, msg, args...)
|
||||||
}
|
}
|
||||||
mockShutdown := func() {
|
mockShutdown := func() {
|
||||||
shutdownCalled = true
|
shutdownCalled = true
|
||||||
|
|
@ -149,7 +149,7 @@ func TestMainPanicRecoveryPattern(t *testing.T) {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
mockLogError("recovered from panic: %v", r)
|
mockLogError("recovered from panic: %v", r)
|
||||||
mockShutdown()
|
mockShutdown()
|
||||||
mockFatal(fmt.Errorf("panic: %v", r))
|
mockFatal(core.NewError(core.Sprintf("panic: %v", r)))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,7 +14,7 @@ import (
|
||||||
// Err creates a new error from a format string.
|
// Err creates a new error from a format string.
|
||||||
// This is a direct replacement for fmt.Errorf.
|
// This is a direct replacement for fmt.Errorf.
|
||||||
func Err(format string, args ...any) error {
|
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.
|
// Wrap wraps an error with a message.
|
||||||
|
|
@ -26,7 +25,7 @@ func Wrap(err error, msg string) error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return 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".
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, subject)
|
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".
|
// WrapAction wraps an error using i18n grammar for "Failed to verb".
|
||||||
|
|
@ -52,7 +51,7 @@ func WrapAction(err error, verb string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, "")
|
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.
|
// Is reports whether any error in err's tree matches target.
|
||||||
// This is a re-export of errors.Is for convenience.
|
// This is a re-export of errors.Is for convenience.
|
||||||
func Is(err, target error) bool {
|
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.
|
// As finds the first error in err's tree that matches target.
|
||||||
// This is a re-export of errors.As for convenience.
|
// This is a re-export of errors.As for convenience.
|
||||||
func As(err error, target any) bool {
|
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.
|
// Join returns an error that wraps the given errors.
|
||||||
// This is a re-export of errors.Join for convenience.
|
// This is a re-export of errors.Join for convenience.
|
||||||
func Join(errs ...error) error {
|
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.
|
// 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) {
|
func Fatal(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Fatal error", "err", err)
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,9 +121,9 @@ func Fatal(err error) {
|
||||||
//
|
//
|
||||||
// Deprecated: return an error from the command instead.
|
// Deprecated: return an error from the command instead.
|
||||||
func Fatalf(format string, args ...any) {
|
func Fatalf(format string, args ...any) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := core.Sprintf(format, args...)
|
||||||
LogError("Fatal error", "msg", msg)
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,8 +138,8 @@ func FatalWrap(err error, msg string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
LogError("Fatal error", "msg", msg, "err", err)
|
LogError("Fatal error", "msg", msg, "err", err)
|
||||||
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
fullMsg := core.Sprintf("%s: %v", msg, err)
|
||||||
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +155,7 @@ func FatalWrapVerb(err error, verb, subject string) {
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, subject)
|
msg := i18n.ActionFailed(verb, subject)
|
||||||
LogError("Fatal error", "msg", msg, "err", err, "verb", verb, "subject", subject)
|
LogError("Fatal error", "msg", msg, "err", err, "verb", verb, "subject", subject)
|
||||||
fullMsg := fmt.Sprintf("%s: %v", msg, err)
|
fullMsg := core.Sprintf("%s: %v", msg, err)
|
||||||
fmt.Fprintln(os.Stderr, ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
core.Print(os.Stderr, "%s\n", ErrorStyle.Render(Glyph(":cross:")+" "+fullMsg))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"golang.org/x/term"
|
"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.
|
// In non-TTY mode, it renders once and returns immediately.
|
||||||
func (f *Frame) Run() {
|
func (f *Frame) Run() {
|
||||||
if !f.isTTY() {
|
if !f.isTTY() {
|
||||||
fmt.Fprint(f.out, f.String())
|
_, _ = io.WriteString(f.out, f.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.runLive()
|
f.runLive()
|
||||||
|
|
@ -429,7 +428,7 @@ func (f *Frame) String() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
// Ensure trailing newline for non-TTY consistency
|
// Ensure trailing newline for non-TTY consistency
|
||||||
if !strings.HasSuffix(view, "\n") {
|
if !core.HasSuffix(view, "\n") {
|
||||||
view += "\n"
|
view += "\n"
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"iter"
|
"iter"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Region represents one of the 5 HLCRF regions.
|
// Region represents one of the 5 HLCRF regions.
|
||||||
|
|
@ -91,7 +92,7 @@ func ParseVariant(variant string) (*Composite, error) {
|
||||||
for i < len(variant) {
|
for i < len(variant) {
|
||||||
r := Region(variant[i])
|
r := Region(variant[i])
|
||||||
if !isValidRegion(r) {
|
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)}
|
slot := &Slot{region: r, path: string(r)}
|
||||||
|
|
@ -101,7 +102,7 @@ func ParseVariant(variant string) (*Composite, error) {
|
||||||
if i < len(variant) && variant[i] == '[' {
|
if i < len(variant) && variant[i] == '[' {
|
||||||
end := findMatchingBracket(variant, i)
|
end := findMatchingBracket(variant, i)
|
||||||
if end == -1 {
|
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])
|
nested, err := ParseVariant(variant[i+1 : end])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -168,6 +169,6 @@ func toRenderable(item any) Renderable {
|
||||||
case string:
|
case string:
|
||||||
return StringBlock(v)
|
return StringBlock(v)
|
||||||
default:
|
default:
|
||||||
return StringBlock(fmt.Sprint(v))
|
return StringBlock(core.Sprint(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,60 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blank prints an empty line.
|
// Blank prints an empty line.
|
||||||
func Blank() {
|
func Blank() {
|
||||||
fmt.Println()
|
core.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Echo translates a key via i18n.T and prints with newline.
|
// Echo translates a key via i18n.T and prints with newline.
|
||||||
// No automatic styling - use Success/Error/Warn/Info for styled output.
|
// No automatic styling - use Success/Error/Warn/Info for styled output.
|
||||||
func Echo(key string, args ...any) {
|
func Echo(key string, args ...any) {
|
||||||
fmt.Println(i18n.T(key, args...))
|
core.Println(i18n.T(key, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print outputs formatted text (no newline).
|
// Print outputs formatted text (no newline).
|
||||||
// Glyph shortcodes like :check: are converted.
|
// Glyph shortcodes like :check: are converted.
|
||||||
func Print(format string, args ...any) {
|
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.
|
// Println outputs formatted text with newline.
|
||||||
// Glyph shortcodes like :check: are converted.
|
// Glyph shortcodes like :check: are converted.
|
||||||
func Println(format string, args ...any) {
|
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.
|
// Text prints arguments like fmt.Println, but handling glyphs.
|
||||||
func Text(args ...any) {
|
func Text(args ...any) {
|
||||||
fmt.Println(compileGlyphs(fmt.Sprint(args...)))
|
core.Println(compileGlyphs(core.Sprint(args...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success prints a success message with checkmark (green).
|
// Success prints a success message with checkmark (green).
|
||||||
func Success(msg string) {
|
func Success(msg string) {
|
||||||
fmt.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg))
|
core.Println(SuccessStyle.Render(Glyph(":check:") + " " + msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successf prints a formatted success message.
|
// Successf prints a formatted success message.
|
||||||
func Successf(format string, args ...any) {
|
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.
|
// Error prints an error message with cross (red) to stderr and logs it.
|
||||||
func Error(msg string) {
|
func Error(msg string) {
|
||||||
LogError(msg)
|
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.
|
// Errorf prints a formatted error message to stderr and logs it.
|
||||||
func Errorf(format string, args ...any) {
|
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.
|
// ErrorWrap prints a wrapped error message to stderr and logs it.
|
||||||
|
|
@ -62,7 +62,7 @@ func ErrorWrap(err error, msg string) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, subject)
|
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.
|
// ErrorWrapAction prints a wrapped error using i18n grammar to stderr and logs it.
|
||||||
|
|
@ -80,33 +80,33 @@ func ErrorWrapAction(err error, verb string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg := i18n.ActionFailed(verb, "")
|
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.
|
// Warn prints a warning message with warning symbol (amber) to stderr and logs it.
|
||||||
func Warn(msg string) {
|
func Warn(msg string) {
|
||||||
LogWarn(msg)
|
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.
|
// Warnf prints a formatted warning message to stderr and logs it.
|
||||||
func Warnf(format string, args ...any) {
|
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).
|
// Info prints an info message with info symbol (blue).
|
||||||
func Info(msg string) {
|
func Info(msg string) {
|
||||||
fmt.Println(InfoStyle.Render(Glyph(":info:") + " " + msg))
|
core.Println(InfoStyle.Render(Glyph(":info:") + " " + msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof prints a formatted info message.
|
// Infof prints a formatted info message.
|
||||||
func Infof(format string, args ...any) {
|
func Infof(format string, args ...any) {
|
||||||
Info(fmt.Sprintf(format, args...))
|
Info(core.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dim prints dimmed text.
|
// Dim prints dimmed text.
|
||||||
func Dim(msg string) {
|
func Dim(msg string) {
|
||||||
fmt.Println(DimStyle.Render(msg))
|
core.Println(DimStyle.Render(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress prints a progress indicator that overwrites the current line.
|
// 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) {
|
func Progress(verb string, current, total int, item ...string) {
|
||||||
msg := i18n.Progress(verb)
|
msg := i18n.Progress(verb)
|
||||||
if len(item) > 0 && item[0] != "" {
|
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 {
|
} 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.
|
// ProgressDone clears the progress line.
|
||||||
func ProgressDone() {
|
func ProgressDone() {
|
||||||
fmt.Print("\033[2K\r")
|
_, _ = io.WriteString(os.Stdout, "\033[2K\r")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label prints a "Label: value" line.
|
// Label prints a "Label: value" line.
|
||||||
func Label(word, value string) {
|
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.
|
// Scanln reads from stdin.
|
||||||
func Scanln(a ...any) (int, error) {
|
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"
|
// 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("php", "Running tests...") // [php] Running tests...
|
||||||
// cli.Task("go", i18n.Progress("build")) // [go] Building...
|
// cli.Task("go", i18n.Progress("build")) // [go] Building...
|
||||||
func Task(label, message string) {
|
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 ──"
|
// Section prints a section header: "── SECTION ──"
|
||||||
//
|
//
|
||||||
// cli.Section("audit") // ── AUDIT ──
|
// cli.Section("audit") // ── AUDIT ──
|
||||||
func Section(name string) {
|
func Section(name string) {
|
||||||
header := "── " + strings.ToUpper(name) + " ──"
|
header := "── " + core.Upper(name) + " ──"
|
||||||
fmt.Println(AccentStyle.Render(header))
|
core.Println(AccentStyle.Render(header))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hint prints a labelled hint: "label: message"
|
// Hint prints a labelled hint: "label: message"
|
||||||
|
|
@ -156,7 +157,7 @@ func Section(name string) {
|
||||||
// cli.Hint("install", "composer require vimeo/psalm")
|
// cli.Hint("install", "composer require vimeo/psalm")
|
||||||
// cli.Hint("fix", "core php fmt --fix")
|
// cli.Hint("fix", "core php fmt --fix")
|
||||||
func Hint(label, message string) {
|
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.
|
// Severity prints a severity-styled message.
|
||||||
|
|
@ -167,7 +168,7 @@ func Hint(label, message string) {
|
||||||
// cli.Severity("low", "Debug enabled") // gray
|
// cli.Severity("low", "Debug enabled") // gray
|
||||||
func Severity(level, message string) {
|
func Severity(level, message string) {
|
||||||
var style *AnsiStyle
|
var style *AnsiStyle
|
||||||
switch strings.ToLower(level) {
|
switch core.Lower(level) {
|
||||||
case "critical":
|
case "critical":
|
||||||
style = NewStyle().Bold().Foreground(ColourRed500)
|
style = NewStyle().Bold().Foreground(ColourRed500)
|
||||||
case "high":
|
case "high":
|
||||||
|
|
@ -179,7 +180,7 @@ func Severity(level, message string) {
|
||||||
default:
|
default:
|
||||||
style = DimStyle
|
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"
|
// Result prints a result line: "✓ message" or "✗ message"
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stdin io.Reader = os.Stdin
|
var stdin io.Reader = os.Stdin
|
||||||
|
|
@ -26,9 +26,9 @@ func newReader() *bufio.Reader {
|
||||||
// Prompt asks for text input with a default value.
|
// Prompt asks for text input with a default value.
|
||||||
func Prompt(label, defaultVal string) (string, error) {
|
func Prompt(label, defaultVal string) (string, error) {
|
||||||
if defaultVal != "" {
|
if defaultVal != "" {
|
||||||
fmt.Printf("%s [%s]: ", label, defaultVal)
|
core.Print(os.Stdout, "%s [%s]: ", label, defaultVal)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s: ", label)
|
core.Print(os.Stdout, "%s: ", label)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := newReader()
|
r := newReader()
|
||||||
|
|
@ -37,7 +37,7 @@ func Prompt(label, defaultVal string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
input = strings.TrimSpace(input)
|
input = core.Trim(input)
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return defaultVal, nil
|
return defaultVal, nil
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +46,11 @@ func Prompt(label, defaultVal string) (string, error) {
|
||||||
|
|
||||||
// Select presents numbered options and returns the selected value.
|
// Select presents numbered options and returns the selected value.
|
||||||
func Select(label string, options []string) (string, error) {
|
func Select(label string, options []string) (string, error) {
|
||||||
fmt.Println(label)
|
core.Println(label)
|
||||||
for i, opt := range options {
|
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()
|
r := newReader()
|
||||||
input, err := r.ReadString('\n')
|
input, err := r.ReadString('\n')
|
||||||
|
|
@ -58,20 +58,20 @@ func Select(label string, options []string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := strconv.Atoi(strings.TrimSpace(input))
|
n, err := strconv.Atoi(core.Trim(input))
|
||||||
if err != nil || n < 1 || n > len(options) {
|
if err != nil || n < 1 || n > len(options) {
|
||||||
return "", errors.New("invalid selection")
|
return "", core.NewError("invalid selection")
|
||||||
}
|
}
|
||||||
return options[n-1], nil
|
return options[n-1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiSelect presents checkboxes (space-separated numbers).
|
// MultiSelect presents checkboxes (space-separated numbers).
|
||||||
func MultiSelect(label string, options []string) ([]string, error) {
|
func MultiSelect(label string, options []string) ([]string, error) {
|
||||||
fmt.Println(label)
|
core.Println(label)
|
||||||
for i, opt := range options {
|
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()
|
r := newReader()
|
||||||
input, err := r.ReadString('\n')
|
input, err := r.ReadString('\n')
|
||||||
|
|
@ -80,7 +80,7 @@ func MultiSelect(label string, options []string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected []string
|
var selected []string
|
||||||
for _, s := range strings.Fields(input) {
|
for s := range strings.FieldsSeq(input) {
|
||||||
n, err := strconv.Atoi(s)
|
n, err := strconv.Atoi(s)
|
||||||
if err != nil || n < 1 || n > len(options) {
|
if err != nil || n < 1 || n > len(options) {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderStyle controls how layouts are rendered.
|
// RenderStyle controls how layouts are rendered.
|
||||||
|
|
@ -31,13 +34,13 @@ func UseRenderBoxed() { currentRenderStyle = RenderBoxed }
|
||||||
|
|
||||||
// Render outputs the layout to terminal.
|
// Render outputs the layout to terminal.
|
||||||
func (c *Composite) Render() {
|
func (c *Composite) Render() {
|
||||||
fmt.Print(c.String())
|
_, _ = io.WriteString(os.Stdout, c.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the rendered layout.
|
// String returns the rendered layout.
|
||||||
func (c *Composite) String() string {
|
func (c *Composite) String() string {
|
||||||
var sb strings.Builder
|
sb := core.NewBuilder()
|
||||||
c.renderTo(&sb, 0)
|
c.renderTo(sb, 0)
|
||||||
return sb.String()
|
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) {
|
func (c *Composite) renderSlot(sb *strings.Builder, slot *Slot, depth int) {
|
||||||
indent := strings.Repeat(" ", depth)
|
indent := strings.Repeat(" ", depth)
|
||||||
for _, block := range slot.blocks {
|
for _, block := range slot.blocks {
|
||||||
for _, line := range strings.Split(block.Render(), "\n") {
|
for _, line := range core.Split(block.Render(), "\n") {
|
||||||
if line != "" {
|
if line != "" {
|
||||||
sb.WriteString(indent + line + "\n")
|
sb.WriteString(indent + line + "\n")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,7 @@ func Init(opts Options) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Core with app identity
|
// Create Core with app identity
|
||||||
c := core.New(core.Options{
|
c := core.New(core.WithOption("name", opts.AppName))
|
||||||
{Key: "name", Value: opts.AppName},
|
|
||||||
})
|
|
||||||
c.App().Version = opts.Version
|
c.App().Version = opts.Version
|
||||||
c.App().Runtime = rootCmd
|
c.App().Runtime = rootCmd
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -59,7 +58,7 @@ func (s *Stream) Write(text string) {
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
if s.wrap <= 0 {
|
if s.wrap <= 0 {
|
||||||
fmt.Fprint(s.out, text)
|
_, _ = io.WriteString(s.out, text)
|
||||||
// Track column across newlines for Done() trailing-newline logic.
|
// Track column across newlines for Done() trailing-newline logic.
|
||||||
if idx := strings.LastIndex(text, "\n"); idx >= 0 {
|
if idx := strings.LastIndex(text, "\n"); idx >= 0 {
|
||||||
s.col = utf8.RuneCountInString(text[idx+1:])
|
s.col = utf8.RuneCountInString(text[idx+1:])
|
||||||
|
|
@ -71,17 +70,17 @@ func (s *Stream) Write(text string) {
|
||||||
|
|
||||||
for _, r := range text {
|
for _, r := range text {
|
||||||
if r == '\n' {
|
if r == '\n' {
|
||||||
fmt.Fprintln(s.out)
|
_, _ = io.WriteString(s.out, "\n")
|
||||||
s.col = 0
|
s.col = 0
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.col >= s.wrap {
|
if s.col >= s.wrap {
|
||||||
fmt.Fprintln(s.out)
|
_, _ = io.WriteString(s.out, "\n")
|
||||||
s.col = 0
|
s.col = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(s.out, string(r))
|
_, _ = io.WriteString(s.out, string(r))
|
||||||
s.col++
|
s.col++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +106,7 @@ func (s *Stream) WriteFrom(r io.Reader) error {
|
||||||
func (s *Stream) Done() {
|
func (s *Stream) Done() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.col > 0 {
|
if s.col > 0 {
|
||||||
fmt.Fprintln(s.out) // ensure trailing newline
|
_, _ = io.WriteString(s.out, "\n") // ensure trailing newline
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
close(s.done)
|
close(s.done)
|
||||||
|
|
@ -125,15 +124,18 @@ func (s *Stream) Column() int {
|
||||||
return s.col
|
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.
|
// 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 {
|
func (s *Stream) Captured() string {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if sb, ok := s.out.(*strings.Builder); ok {
|
if sb, ok := s.out.(*strings.Builder); ok {
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
if st, ok := s.out.(fmt.Stringer); ok {
|
if st, ok := s.out.(stringer); ok {
|
||||||
return st.String()
|
return st.String()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
package cli
|
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 {
|
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 {
|
func Sprint(args ...any) string {
|
||||||
return fmt.Sprint(args...)
|
return core.Sprint(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styled returns text with a style applied.
|
// 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.
|
// Styledf returns formatted text with a style applied.
|
||||||
func Styledf(style *AnsiStyle, format string, args ...any) string {
|
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.
|
// SuccessStr returns success-styled string.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tailwind colour palette (hex strings)
|
// Tailwind colour palette (hex strings)
|
||||||
|
|
@ -93,15 +96,15 @@ func FormatAge(t time.Time) string {
|
||||||
case d < time.Minute:
|
case d < time.Minute:
|
||||||
return "just now"
|
return "just now"
|
||||||
case d < time.Hour:
|
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:
|
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:
|
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:
|
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:
|
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.
|
// Render prints the table to stdout.
|
||||||
func (t *Table) Render() {
|
func (t *Table) Render() {
|
||||||
fmt.Print(t.String())
|
_, _ = io.WriteString(os.Stdout, t.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) colCount() int {
|
func (t *Table) colCount() int {
|
||||||
|
|
@ -315,7 +318,7 @@ func (t *Table) resolveStyle(col int, value string) *AnsiStyle {
|
||||||
func (t *Table) renderPlain() string {
|
func (t *Table) renderPlain() string {
|
||||||
widths := t.columnWidths()
|
widths := t.columnWidths()
|
||||||
|
|
||||||
var sb strings.Builder
|
sb := core.NewBuilder()
|
||||||
sep := t.Style.Separator
|
sep := t.Style.Separator
|
||||||
|
|
||||||
if len(t.Headers) > 0 {
|
if len(t.Headers) > 0 {
|
||||||
|
|
@ -358,7 +361,7 @@ func (t *Table) renderBordered() string {
|
||||||
widths := t.columnWidths()
|
widths := t.columnWidths()
|
||||||
cols := t.colCount()
|
cols := t.colCount()
|
||||||
|
|
||||||
var sb strings.Builder
|
sb := core.NewBuilder()
|
||||||
|
|
||||||
// Top border: ╭──────┬──────╮
|
// Top border: ╭──────┬──────╮
|
||||||
sb.WriteString(b.tl)
|
sb.WriteString(b.tl)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
"iter"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -171,7 +170,7 @@ func (tr *TaskTracker) waitStatic() {
|
||||||
if state == taskFailed {
|
if state == taskFailed {
|
||||||
icon = Glyph(":cross:")
|
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 {
|
if allDone {
|
||||||
return
|
return
|
||||||
|
|
@ -203,7 +202,7 @@ func (tr *TaskTracker) waitLive() {
|
||||||
tr.mu.Unlock()
|
tr.mu.Unlock()
|
||||||
|
|
||||||
// Move cursor up to redraw all lines.
|
// 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 {
|
for i := range count {
|
||||||
tr.renderLine(i, frame)
|
tr.renderLine(i, frame)
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +243,7 @@ func (tr *TaskTracker) renderLine(idx, frame int) {
|
||||||
styledStatus = DimStyle.Render(status)
|
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 {
|
func (tr *TaskTracker) nameWidth() int {
|
||||||
|
|
@ -289,9 +288,9 @@ func (tr *TaskTracker) Summary() string {
|
||||||
|
|
||||||
total := len(tr.tasks)
|
total := len(tr.tasks)
|
||||||
if failed > 0 {
|
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).
|
// 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()
|
tr.mu.Unlock()
|
||||||
|
|
||||||
nameW := tr.nameWidth()
|
nameW := tr.nameWidth()
|
||||||
var sb strings.Builder
|
sb := core.NewBuilder()
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
name, status, state := t.snapshot()
|
name, status, state := t.snapshot()
|
||||||
icon := "…"
|
icon := "…"
|
||||||
|
|
@ -313,7 +312,7 @@ func (tr *TaskTracker) String() string {
|
||||||
case taskRunning:
|
case taskRunning:
|
||||||
icon = "⠋"
|
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()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
"iter"
|
"iter"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TreeNode represents a node in a displayable tree structure.
|
// 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.
|
// String renders the tree with box-drawing characters.
|
||||||
// Implements fmt.Stringer.
|
// Implements fmt.Stringer.
|
||||||
func (n *TreeNode) String() string {
|
func (n *TreeNode) String() string {
|
||||||
var sb strings.Builder
|
sb := core.NewBuilder()
|
||||||
sb.WriteString(n.renderLabel())
|
sb.WriteString(n.renderLabel())
|
||||||
sb.WriteByte('\n')
|
sb.WriteByte('\n')
|
||||||
n.writeChildren(&sb, "")
|
n.writeChildren(sb, "")
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render prints the tree to stdout.
|
// Render prints the tree to stdout.
|
||||||
func (n *TreeNode) Render() {
|
func (n *TreeNode) Render() {
|
||||||
fmt.Print(n.String())
|
_, _ = io.WriteString(os.Stdout, n.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *TreeNode) renderLabel() string {
|
func (n *TreeNode) renderLabel() string {
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,26 @@ package cli
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
"forge.lthn.ai/core/go-i18n"
|
"forge.lthn.ai/core/go-i18n"
|
||||||
"forge.lthn.ai/core/go-log"
|
"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.
|
// GhAuthenticated checks if the GitHub CLI is authenticated.
|
||||||
// Returns true if 'gh auth status' indicates a logged-in user.
|
// Returns true if 'gh auth status' indicates a logged-in user.
|
||||||
func GhAuthenticated() bool {
|
func GhAuthenticated() bool {
|
||||||
cmd := exec.Command("gh", "auth", "status")
|
cmd := exec.Command("gh", "auth", "status")
|
||||||
output, _ := cmd.CombinedOutput()
|
output, _ := cmd.CombinedOutput()
|
||||||
authenticated := strings.Contains(string(output), "Logged in")
|
authenticated := core.Contains(string(output), "Logged in")
|
||||||
|
|
||||||
if authenticated {
|
if authenticated {
|
||||||
LogWarn("GitHub CLI authenticated", "user", log.Username())
|
LogWarn("GitHub CLI authenticated", "user", log.Username())
|
||||||
|
|
@ -94,13 +97,13 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
||||||
|
|
||||||
// Add timeout indicator if set
|
// Add timeout indicator if set
|
||||||
if cfg.timeout > 0 {
|
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)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Printf("%s %s", prompt, suffix)
|
core.Print(os.Stdout, "%s %s", prompt, suffix)
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
|
|
||||||
|
|
@ -114,14 +117,14 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case response = <-resultChan:
|
case response = <-resultChan:
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
response = core.Lower(core.Trim(response))
|
||||||
case <-time.After(cfg.timeout):
|
case <-time.After(cfg.timeout):
|
||||||
fmt.Println() // New line after timeout
|
core.Println() // New line after timeout
|
||||||
return cfg.defaultYes
|
return cfg.defaultYes
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response, _ = reader.ReadString('\n')
|
response, _ = reader.ReadString('\n')
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
response = core.Lower(core.Trim(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle empty response
|
// Handle empty response
|
||||||
|
|
@ -142,7 +145,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
||||||
|
|
||||||
// Invalid response
|
// Invalid response
|
||||||
if cfg.required {
|
if cfg.required {
|
||||||
fmt.Println("Please enter 'y' or 'n'")
|
core.Println("Please enter 'y' or 'n'")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,18 +223,18 @@ func Question(prompt string, opts ...QuestionOption) string {
|
||||||
for {
|
for {
|
||||||
// Build prompt with default
|
// Build prompt with default
|
||||||
if cfg.defaultValue != "" {
|
if cfg.defaultValue != "" {
|
||||||
fmt.Printf("%s [%s] ", prompt, cfg.defaultValue)
|
core.Print(os.Stdout, "%s [%s] ", prompt, cfg.defaultValue)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s ", prompt)
|
core.Print(os.Stdout, "%s ", prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, _ := reader.ReadString('\n')
|
response, _ := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(response)
|
response = core.Trim(response)
|
||||||
|
|
||||||
// Handle empty response
|
// Handle empty response
|
||||||
if response == "" {
|
if response == "" {
|
||||||
if cfg.required {
|
if cfg.required {
|
||||||
fmt.Println("Response required")
|
core.Println("Response required")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
response = cfg.defaultValue
|
response = cfg.defaultValue
|
||||||
|
|
@ -240,7 +243,7 @@ func Question(prompt string, opts ...QuestionOption) string {
|
||||||
// Validate if validator provided
|
// Validate if validator provided
|
||||||
if cfg.validator != nil {
|
if cfg.validator != nil {
|
||||||
if err := cfg.validator(response); err != nil {
|
if err := cfg.validator(response); err != nil {
|
||||||
fmt.Printf("Invalid: %v\n", err)
|
core.Print(os.Stdout, "Invalid: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,28 +322,28 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &chooseConfig[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 {
|
for _, opt := range opts {
|
||||||
opt(cfg)
|
opt(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display options
|
// Display options
|
||||||
fmt.Println(prompt)
|
core.Println(prompt)
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
marker := " "
|
marker := " "
|
||||||
if i == cfg.defaultN {
|
if i == cfg.defaultN {
|
||||||
marker = "*"
|
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)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Printf("Enter number [1-%d]: ", len(items))
|
core.Print(os.Stdout, "Enter number [1-%d]: ", len(items))
|
||||||
response, _ := reader.ReadString('\n')
|
response, _ := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(response)
|
response = core.Trim(response)
|
||||||
|
|
||||||
// Empty response uses default
|
// Empty response uses default
|
||||||
if response == "" {
|
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]{
|
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 {
|
for _, opt := range opts {
|
||||||
opt(cfg)
|
opt(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display options
|
// Display options
|
||||||
fmt.Println(prompt)
|
core.Println(prompt)
|
||||||
for i, item := range items {
|
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)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
for {
|
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, _ := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(response)
|
response = core.Trim(response)
|
||||||
|
|
||||||
// Empty response returns no selections
|
// Empty response returns no selections
|
||||||
if response == "" {
|
if response == "" {
|
||||||
|
|
@ -411,7 +414,7 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
|
||||||
// Parse the selection
|
// Parse the selection
|
||||||
selected, err := parseMultiSelection(response, len(items))
|
selected, err := parseMultiSelection(response, len(items))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Invalid selection: %v\n", err)
|
core.Print(os.Stdout, "Invalid selection: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,23 +434,23 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
|
||||||
|
|
||||||
for part := range strings.FieldsSeq(input) {
|
for part := range strings.FieldsSeq(input) {
|
||||||
// Check for range (e.g., "1-3")
|
// Check for range (e.g., "1-3")
|
||||||
if strings.Contains(part, "-") {
|
if core.Contains(part, "-") {
|
||||||
var rangeParts []string
|
var rangeParts []string
|
||||||
for p := range strings.SplitSeq(part, "-") {
|
for p := range strings.SplitSeq(part, "-") {
|
||||||
rangeParts = append(rangeParts, p)
|
rangeParts = append(rangeParts, p)
|
||||||
}
|
}
|
||||||
if len(rangeParts) != 2 {
|
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
|
var start, end int
|
||||||
if _, err := fmt.Sscanf(rangeParts[0], "%d", &start); err != nil {
|
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 {
|
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 {
|
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++ {
|
for i := start; i <= end; i++ {
|
||||||
selected[i-1] = true // Convert to 0-based
|
selected[i-1] = true // Convert to 0-based
|
||||||
|
|
@ -456,10 +459,10 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
|
||||||
// Single number
|
// Single number
|
||||||
var n int
|
var n int
|
||||||
if _, err := fmt.Sscanf(part, "%d", &n); err != nil {
|
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 {
|
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
|
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.
|
// Prefers 'gh repo clone' if authenticated, falls back to SSH.
|
||||||
func GitClone(ctx context.Context, org, repo, path string) error {
|
func GitClone(ctx context.Context, org, repo, path string) error {
|
||||||
if GhAuthenticated() {
|
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)
|
cmd := exec.CommandContext(ctx, "gh", "repo", "clone", httpsURL, path)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
errStr := strings.TrimSpace(string(output))
|
errStr := core.Trim(string(output))
|
||||||
if strings.Contains(errStr, "already exists") {
|
if core.Contains(errStr, "already exists") {
|
||||||
return errors.New(errStr)
|
return core.NewError(errStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to SSH clone
|
// 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()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(strings.TrimSpace(string(output)))
|
return core.NewError(core.Trim(string(output)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue