mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
- Migrate module path to github.com/hanzoai/pack across all Go packages - Remove legacy upstream docs and competitor brand references - Modernize LLM.md and mise.toml for pristine Hanzo Pack builder
362 lines
9.1 KiB
Go
362 lines
9.1 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/termenv"
|
|
"github.com/hanzoai/pack/core/logger"
|
|
"github.com/hanzoai/pack/core/plan"
|
|
"github.com/hanzoai/pack/core/resolver"
|
|
"github.com/hanzoai/pack/internal/utils"
|
|
"github.com/tidwall/pretty"
|
|
)
|
|
|
|
const (
|
|
AnsiRed = "1"
|
|
AnsiYellow = "3"
|
|
AnsiBlue = "4"
|
|
AnsiMagenta = "5"
|
|
AnsiCyan = "6"
|
|
AnsiWhite = "7"
|
|
AnsiGray = "8"
|
|
AnsiBrightBlue = "12"
|
|
AnsiBrightCyan = "14"
|
|
AnsiBrightWhite = "15"
|
|
AnsiBrightMagenta = "13"
|
|
AnsiDarkGray = "238"
|
|
AnsiMediumGray = "245"
|
|
)
|
|
|
|
var (
|
|
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
|
|
|
|
headerStyle = lipgloss.NewStyle().
|
|
BorderStyle(lipgloss.RoundedBorder()).
|
|
BorderForeground(highlight).
|
|
MarginTop(1).
|
|
Padding(0, 1)
|
|
|
|
sectionHeaderStyle = lipgloss.NewStyle().
|
|
Bold(true).
|
|
Width(10).
|
|
MarginLeft(2).
|
|
MarginTop(2).
|
|
BorderStyle(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color(AnsiDarkGray)).
|
|
BorderBottom(true)
|
|
|
|
packageNameStyle = lipgloss.NewStyle().
|
|
MarginLeft(2).
|
|
Foreground(lipgloss.Color(AnsiBrightMagenta))
|
|
|
|
versionStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiBrightCyan))
|
|
|
|
sourceStyle = lipgloss.NewStyle()
|
|
|
|
separatorStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiDarkGray)).
|
|
Margin(0, 2)
|
|
|
|
indentedStepHeaderStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiBrightMagenta)).
|
|
MarginLeft(2)
|
|
|
|
commandPrefixStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiMediumGray)).
|
|
MarginLeft(4)
|
|
|
|
commandStyle = lipgloss.NewStyle().
|
|
Bold(true)
|
|
|
|
logInfoStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiBrightWhite)).
|
|
MarginLeft(2)
|
|
|
|
logWarnStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiYellow)).
|
|
MarginLeft(2)
|
|
|
|
logDeprecationStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiBrightMagenta)).
|
|
MarginLeft(2)
|
|
|
|
logSuggestionStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiCyan)).
|
|
MarginLeft(2)
|
|
|
|
logDocsLinkStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiBrightBlue)).
|
|
Underline(true)
|
|
|
|
logErrorStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiRed)).
|
|
Bold(true).
|
|
MarginLeft(2)
|
|
|
|
metadataStyle = lipgloss.NewStyle().
|
|
MarginLeft(2)
|
|
|
|
metadataSeparatorStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(AnsiMediumGray)).
|
|
MarginRight(1)
|
|
|
|
metadataValueStyle = lipgloss.NewStyle().
|
|
Bold(true)
|
|
|
|
boxStyle = lipgloss.NewStyle().
|
|
BorderStyle(lipgloss.RoundedBorder()).
|
|
BorderForeground(highlight).
|
|
MarginTop(1).
|
|
Padding(0, 1)
|
|
|
|
highlightedTextStyle = lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color(AnsiBrightCyan))
|
|
)
|
|
|
|
type PrintOptions struct {
|
|
Metadata bool
|
|
Version string
|
|
}
|
|
|
|
func PrettyPrintBuildResult(buildResult *BuildResult, options ...PrintOptions) {
|
|
output := FormatBuildResult(buildResult, options...)
|
|
fmt.Print(output)
|
|
}
|
|
|
|
func PrettyPrintSectionHeader(output io.Writer, title string) {
|
|
style := sectionHeaderStyle.
|
|
MarginTop(0).
|
|
Width(max(10, lipgloss.Width(title)))
|
|
_, _ = fmt.Fprintf(output, "%s\n\n", style.Render(title))
|
|
}
|
|
|
|
func PrettyPrintBox(content string) {
|
|
fmt.Println(boxStyle.Render(content))
|
|
}
|
|
|
|
func FormatHighlight(content string) string {
|
|
return highlightedTextStyle.Render(content)
|
|
}
|
|
|
|
func PrettyPrintJSON(output io.Writer, content []byte) {
|
|
if lipgloss.ColorProfile() != termenv.Ascii {
|
|
content = pretty.Color(content, nil)
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(output, string(content))
|
|
}
|
|
|
|
func FormatBuildResult(br *BuildResult, options ...PrintOptions) string {
|
|
var opts PrintOptions
|
|
if len(options) > 0 {
|
|
opts = options[0]
|
|
}
|
|
var output strings.Builder
|
|
|
|
formatHeader(&output, opts.Version)
|
|
formatLogs(&output, br.Logs)
|
|
formatPackages(&output, br.ResolvedPackages)
|
|
formatSteps(&output, br)
|
|
formatDeploy(&output, br)
|
|
formatMetadata(&output, br.Metadata, opts.Metadata)
|
|
|
|
output.WriteString("\n\n")
|
|
return output.String()
|
|
}
|
|
|
|
func formatHeader(output *strings.Builder, version string) {
|
|
header := fmt.Sprintf("Railpack %s", version)
|
|
output.WriteString(headerStyle.Render(header))
|
|
output.WriteString("\n")
|
|
}
|
|
|
|
func formatLogs(output *strings.Builder, logs []logger.Msg) {
|
|
if len(logs) == 0 {
|
|
return
|
|
}
|
|
|
|
output.WriteString("\n")
|
|
|
|
for _, log := range logs {
|
|
msg := utils.CapitalizeFirst(log.Msg)
|
|
|
|
switch log.Level {
|
|
case logger.Info:
|
|
output.WriteString(logInfoStyle.Render(fmt.Sprintf("↳ %s", msg)))
|
|
case logger.Warn:
|
|
output.WriteString(logWarnStyle.Render(fmt.Sprintf("⚠ %s", msg)))
|
|
case logger.Deprecation:
|
|
output.WriteString(logDeprecationStyle.Render(fmt.Sprintf("⚑ Deprecated: %s", msg)))
|
|
case logger.Suggestion:
|
|
rendered := logSuggestionStyle.Render(fmt.Sprintf("→ %s", msg))
|
|
if log.DocsPath != "" {
|
|
rendered += " " + logDocsLinkStyle.Render(logger.DocsURL(log.DocsPath))
|
|
}
|
|
output.WriteString(rendered)
|
|
case logger.Error:
|
|
lines := strings.Split(msg, "\n")
|
|
for i, line := range lines {
|
|
if i == 0 {
|
|
output.WriteString(logErrorStyle.Render(fmt.Sprintf("✖ %s", line)))
|
|
} else {
|
|
fmt.Fprintf(output, " %s", line)
|
|
}
|
|
if i < len(lines)-1 {
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
default:
|
|
output.WriteString(logInfoStyle.Render(fmt.Sprintf("• %s", msg)))
|
|
}
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
func formatPackages(output *strings.Builder, packages map[string]*resolver.ResolvedPackage) {
|
|
if len(packages) == 0 {
|
|
return
|
|
}
|
|
|
|
output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Packages"))
|
|
output.WriteString("\n")
|
|
|
|
nameWidth, versionWidth := 1, 1
|
|
for _, pkg := range packages {
|
|
nameWidth = max(nameWidth, len(pkg.Name))
|
|
if pkg.ResolvedVersion != nil {
|
|
versionWidth = max(versionWidth, len(*pkg.ResolvedVersion))
|
|
}
|
|
}
|
|
|
|
localPackageNameStyle := packageNameStyle.Width(nameWidth).MaxWidth(20)
|
|
localVersionStyle := versionStyle.Width(versionWidth).MaxWidth(20)
|
|
separator := separatorStyle.Render("│")
|
|
|
|
for _, pkg := range packages {
|
|
name := localPackageNameStyle.Render(pkg.Name)
|
|
|
|
version := "-"
|
|
if pkg.ResolvedVersion != nil {
|
|
version = *pkg.ResolvedVersion
|
|
}
|
|
version = localVersionStyle.Render(version)
|
|
source := sourceStyle.Render(formatSource(pkg))
|
|
fmt.Fprintf(output, "%s%s%s%s%s", name, separator, version, separator, source)
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
func formatSteps(output *strings.Builder, br *BuildResult) {
|
|
stepsToPrint := getStepsToPrint(br)
|
|
if len(stepsToPrint) == 0 {
|
|
return
|
|
}
|
|
|
|
output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Steps"))
|
|
output.WriteString("\n")
|
|
|
|
for i, step := range stepsToPrint {
|
|
commands := getCommandsToPrint(step.Commands)
|
|
if len(commands) == 0 {
|
|
continue
|
|
}
|
|
|
|
currentStepStyle := indentedStepHeaderStyle
|
|
if i > 0 {
|
|
currentStepStyle = currentStepStyle.MarginTop(1)
|
|
}
|
|
|
|
output.WriteString(currentStepStyle.Render(fmt.Sprintf("▸ %s", step.Name)))
|
|
output.WriteString("\n")
|
|
|
|
for _, cmd := range commands {
|
|
cmdText := cmd.Cmd
|
|
if cmd.CustomName != "" {
|
|
cmdText = cmd.CustomName
|
|
}
|
|
fmt.Fprintf(output, "%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(cmdText))
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatDeploy(output *strings.Builder, br *BuildResult) {
|
|
if br.Plan != nil && br.Plan.Deploy.StartCmd != "" {
|
|
output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Deploy"))
|
|
output.WriteString("\n")
|
|
fmt.Fprintf(output, "%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(br.Plan.Deploy.StartCmd))
|
|
}
|
|
}
|
|
|
|
func formatMetadata(output *strings.Builder, metadata map[string]string, showMetadata bool) {
|
|
if !showMetadata || metadata == nil || len(metadata) == 0 {
|
|
return
|
|
}
|
|
|
|
output.WriteString(sectionHeaderStyle.MarginTop(2).Render("Metadata"))
|
|
output.WriteString("\n")
|
|
|
|
separator := metadataSeparatorStyle.Render(":")
|
|
|
|
for key, value := range metadata {
|
|
output.WriteString(metadataStyle.Render(fmt.Sprintf("%s%s%s", key, separator, metadataValueStyle.Render(value))))
|
|
output.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
func getStepsToPrint(br *BuildResult) []*plan.Step {
|
|
execSteps := []*plan.Step{}
|
|
if br.Plan == nil {
|
|
return execSteps
|
|
}
|
|
|
|
for _, step := range br.Plan.Steps {
|
|
if !strings.HasPrefix(step.Name, "packages") && step.Commands != nil {
|
|
commands := getCommandsToPrint(step.Commands)
|
|
if len(commands) > 0 {
|
|
execSteps = append(execSteps, &step)
|
|
}
|
|
}
|
|
}
|
|
return execSteps
|
|
}
|
|
|
|
// TODO we should really just mark a command as skippable on the ExecCommand instead
|
|
var skippableCommands = []string{
|
|
"mkdir -p /app/node_modules/.cache",
|
|
"mkdir -p config deps _build",
|
|
}
|
|
|
|
func getCommandsToPrint(commands []plan.Command) []plan.ExecCommand {
|
|
if commands == nil {
|
|
return []plan.ExecCommand{}
|
|
}
|
|
|
|
execCommands := []plan.ExecCommand{}
|
|
for _, cmd := range commands {
|
|
if execCmd, ok := cmd.(plan.ExecCommand); ok {
|
|
if !isSkippableCommand(execCmd.Cmd) {
|
|
execCommands = append(execCommands, execCmd)
|
|
}
|
|
}
|
|
}
|
|
return execCommands
|
|
}
|
|
|
|
func isSkippableCommand(cmd string) bool {
|
|
return slices.Contains(skippableCommands, cmd)
|
|
}
|
|
|
|
func formatSource(pkg *resolver.ResolvedPackage) string {
|
|
if pkg.RequestedVersion != nil {
|
|
return fmt.Sprintf("%s (%s)", pkg.Source, *pkg.RequestedVersion)
|
|
}
|
|
return pkg.Source
|
|
}
|