Skip to content

Commit b540fec

Browse files
authored
Replace fatih/color with in-tree ANSI helpers (#5178)
## Summary - Drops `github.com/fatih/color` as a direct dependency by migrating its ~14 call sites (bundle render, bundle run, cfgpickers, logstream, cmd/labs, experimental/aitools, experimental/ssh, python_mutator) to a small ANSI helper set. - Adds `libs/cmdio/color.go` with `cmdio.Red(ctx, msg)`-style helpers and a `RenderFuncMap(ctx)` for templates. The gate matches fatih/color's historical stdout-TTY decision and degrades to plain text when ctx has no cmdIO attached. - Stacks on top of #5170. ANSI constants shared across both colorizers now live in `libs/cmdio/color.go`. No user-visible output changes — the new helpers emit byte-identical SGR sequences. ## Test plan - [ ] Manual smoke: `databricks bundle validate` against a bundle with errors and warnings (colored summary on TTY, uncolored when piped) and `databricks current-user me -o json` (colored on TTY, uncolored when piped through `jq`). This pull request and its description were written by Isaac.
1 parent e76361a commit b540fec

32 files changed

Lines changed: 366 additions & 252 deletions

NOTICE

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,6 @@ charmbracelet/lipgloss - https://github.com/charmbracelet/lipgloss
147147
Copyright (c) 2021-2025 Charmbracelet, Inc
148148
License - https://github.com/charmbracelet/lipgloss/blob/master/LICENSE
149149

150-
fatih/color - https://github.com/fatih/color
151-
Copyright (c) 2013 Fatih Arslan
152-
License - https://github.com/fatih/color/blob/main/LICENSE.md
153-
154150
Masterminds/semver - https://github.com/Masterminds/semver
155151
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
156152
License - https://github.com/Masterminds/semver/blob/master/LICENSE.txt

bundle/config/mutator/python/python_mutator.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import (
1818
"github.com/databricks/cli/libs/log"
1919
"github.com/databricks/cli/libs/logdiag"
2020

21+
"github.com/databricks/cli/libs/cmdio"
2122
"github.com/databricks/databricks-sdk-go/logger"
22-
"github.com/fatih/color"
2323

2424
"github.com/databricks/cli/libs/python"
2525

@@ -386,7 +386,7 @@ func (m *pythonMutator) runPythonMutator(ctx context.Context, root dyn.Value, op
386386
diagnostic := diag.Diagnostic{
387387
Severity: diag.Error,
388388
Summary: fmt.Sprintf("python mutator process failed: %q, use --debug to enable logging", processErr),
389-
Detail: explainProcessErr(stderrBuf.String()),
389+
Detail: explainProcessErr(ctx, stderrBuf.String()),
390390
}
391391

392392
return dyn.InvalidValue, diag.Diagnostics{diagnostic}
@@ -424,10 +424,10 @@ or activate the environment before running CLI commands:
424424
// explainProcessErr provides additional explanation for common errors.
425425
// It's meant to be the best effort, and not all errors are covered.
426426
// Output should be used only used for error reporting.
427-
func explainProcessErr(stderr string) string {
427+
func explainProcessErr(ctx context.Context, stderr string) string {
428428
// implemented in cpython/Lib/runpy.py and portable across Python 3.x, including pypy
429429
if strings.Contains(stderr, "Error while finding module specification for 'databricks.bundles.build'") {
430-
summary := color.CyanString("Explanation: ") + "'databricks-bundles' library is not installed in the Python environment.\n"
430+
summary := cmdio.Cyan(ctx, "Explanation: ") + "'databricks-bundles' library is not installed in the Python environment.\n"
431431

432432
return stderr + "\n" + summary + "\n" + pythonInstallExplanation
433433
}

bundle/config/mutator/python/python_mutator_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/databricks/cli/bundle"
2222
"github.com/databricks/cli/bundle/config"
23+
"github.com/databricks/cli/libs/cmdio"
2324
"github.com/databricks/cli/libs/process"
2425
"github.com/stretchr/testify/assert"
2526
)
@@ -488,7 +489,7 @@ or activate the environment before running CLI commands:
488489
venv_path: .venv
489490
`
490491

491-
out := explainProcessErr(stderr)
492+
out := explainProcessErr(cmdio.MockDiscard(t.Context()), stderr)
492493

493494
assert.Equal(t, expected, out)
494495
}

bundle/render/render_text_output.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,11 @@ import (
1010
"text/template"
1111

1212
"github.com/databricks/cli/bundle"
13+
"github.com/databricks/cli/libs/cmdio"
1314
"github.com/databricks/cli/libs/logdiag"
1415
"github.com/databricks/databricks-sdk-go/service/iam"
15-
"github.com/fatih/color"
1616
)
1717

18-
var renderFuncMap = template.FuncMap{
19-
"red": color.RedString,
20-
"green": color.GreenString,
21-
"blue": color.BlueString,
22-
"yellow": color.YellowString,
23-
"magenta": color.MagentaString,
24-
"cyan": color.CyanString,
25-
"bold": func(format string, a ...any) string {
26-
return color.New(color.Bold).Sprintf(format, a...)
27-
},
28-
"italic": func(format string, a ...any) string {
29-
return color.New(color.Italic).Sprintf(format, a...)
30-
},
31-
}
32-
3318
const summaryHeaderTemplate = `{{- if .Name -}}
3419
Name: {{ .Name | bold }}
3520
{{- if .Target }}
@@ -82,13 +67,13 @@ func buildTrailer(ctx context.Context) string {
8267
info := logdiag.Copy(ctx)
8368
var parts []string
8469
if info.Errors > 0 {
85-
parts = append(parts, color.RedString(pluralize(info.Errors, "error", "errors")))
70+
parts = append(parts, cmdio.Red(ctx, pluralize(info.Errors, "error", "errors")))
8671
}
8772
if info.Warnings > 0 {
88-
parts = append(parts, color.YellowString(pluralize(info.Warnings, "warning", "warnings")))
73+
parts = append(parts, cmdio.Yellow(ctx, pluralize(info.Warnings, "warning", "warnings")))
8974
}
9075
if info.Recommendations > 0 {
91-
parts = append(parts, color.BlueString(pluralize(info.Recommendations, "recommendation", "recommendations")))
76+
parts = append(parts, cmdio.Blue(ctx, pluralize(info.Recommendations, "recommendation", "recommendations")))
9277
}
9378
switch {
9479
case len(parts) >= 3:
@@ -101,7 +86,7 @@ func buildTrailer(ctx context.Context) string {
10186
return fmt.Sprintf("Found %s\n", parts[0])
10287
default:
10388
// No diagnostics to print.
104-
return color.GreenString("Validation OK!\n")
89+
return cmdio.Green(ctx, "Validation OK!\n")
10590
}
10691
}
10792

@@ -118,7 +103,7 @@ func renderSummaryHeaderTemplate(ctx context.Context, out io.Writer, b *bundle.B
118103
}
119104
}
120105

121-
t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryHeaderTemplate))
106+
t := template.Must(template.New("summary").Funcs(cmdio.RenderFuncMap(ctx)).Parse(summaryHeaderTemplate))
122107
err := t.Execute(out, map[string]any{
123108
"Name": b.Config.Bundle.Name,
124109
"Target": b.Config.Bundle.Target,
@@ -179,15 +164,15 @@ func RenderSummary(ctx context.Context, out io.Writer, b *bundle.Bundle) error {
179164
}
180165
}
181166

182-
if err := renderResourcesTemplate(out, resourceGroups); err != nil {
167+
if err := renderResourcesTemplate(ctx, out, resourceGroups); err != nil {
183168
return fmt.Errorf("failed to render resources template: %w", err)
184169
}
185170

186171
return nil
187172
}
188173

189174
// Helper function to sort and render resource groups using the template
190-
func renderResourcesTemplate(out io.Writer, resourceGroups []ResourceGroup) error {
175+
func renderResourcesTemplate(ctx context.Context, out io.Writer, resourceGroups []ResourceGroup) error {
191176
// Sort everything to ensure consistent output
192177
slices.SortFunc(resourceGroups, func(a, b ResourceGroup) int {
193178
return cmp.Compare(a.GroupName, b.GroupName)
@@ -198,7 +183,7 @@ func renderResourcesTemplate(out io.Writer, resourceGroups []ResourceGroup) erro
198183
})
199184
}
200185

201-
t := template.Must(template.New("resources").Funcs(renderFuncMap).Parse(resourcesTemplate))
186+
t := template.Must(template.New("resources").Funcs(cmdio.RenderFuncMap(ctx)).Parse(resourcesTemplate))
202187

203188
return t.Execute(out, resourceGroups)
204189
}

bundle/render/render_text_output_test.go

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,20 @@ import (
1818
"github.com/databricks/databricks-sdk-go/service/jobs"
1919
"github.com/databricks/databricks-sdk-go/service/pipelines"
2020
"github.com/databricks/databricks-sdk-go/service/serving"
21-
"github.com/fatih/color"
2221
"github.com/stretchr/testify/assert"
2322
"github.com/stretchr/testify/require"
2423
)
2524

2625
func TestRenderSummaryHeaderTemplate_nilBundle(t *testing.T) {
2726
writer := &bytes.Buffer{}
2827

29-
err := renderSummaryHeaderTemplate(t.Context(), writer, nil)
28+
err := renderSummaryHeaderTemplate(cmdio.MockDiscard(t.Context()), writer, nil)
3029
require.NoError(t, err)
3130

3231
assert.Equal(t, "", writer.String())
3332
}
3433

3534
func TestRenderDiagnosticsSummary(t *testing.T) {
36-
// Disable colors for consistent test output
37-
oldNoColor := color.NoColor
38-
color.NoColor = true
39-
defer func() {
40-
color.NoColor = oldNoColor
41-
}()
42-
4335
testCases := []struct {
4436
name string
4537
bundle *bundle.Bundle
@@ -114,7 +106,7 @@ func TestRenderDiagnosticsSummary(t *testing.T) {
114106

115107
for _, tc := range testCases {
116108
t.Run(tc.name, func(t *testing.T) {
117-
ctx := logdiag.InitContext(t.Context())
109+
ctx := logdiag.InitContext(cmdio.MockDiscard(t.Context()))
118110
logdiag.SetCollect(ctx, true) // Collect diagnostics instead of outputting to stderr
119111

120112
// Simulate diagnostic counts by logging fake diagnostics
@@ -144,13 +136,6 @@ type renderDiagnosticsTestCase struct {
144136
}
145137

146138
func TestRenderDiagnostics(t *testing.T) {
147-
// Disable colors for consistent test output
148-
oldNoColor := color.NoColor
149-
color.NoColor = true
150-
defer func() {
151-
color.NoColor = oldNoColor
152-
}()
153-
154139
testCases := []renderDiagnosticsTestCase{
155140
{
156141
name: "empty diagnostics",
@@ -286,14 +271,7 @@ func TestRenderDiagnostics(t *testing.T) {
286271
}
287272

288273
func TestRenderSummaryTemplate_nilBundle(t *testing.T) {
289-
// Disable colors for consistent test output
290-
oldNoColor := color.NoColor
291-
color.NoColor = true
292-
defer func() {
293-
color.NoColor = oldNoColor
294-
}()
295-
296-
ctx := logdiag.InitContext(t.Context())
274+
ctx := logdiag.InitContext(cmdio.MockDiscard(t.Context()))
297275
writer := &bytes.Buffer{}
298276

299277
err := renderSummaryHeaderTemplate(ctx, writer, nil)
@@ -306,14 +284,7 @@ func TestRenderSummaryTemplate_nilBundle(t *testing.T) {
306284
}
307285

308286
func TestRenderSummary(t *testing.T) {
309-
ctx := t.Context()
310-
311-
// Disable colors for consistent test output
312-
oldNoColor := color.NoColor
313-
color.NoColor = true
314-
defer func() {
315-
color.NoColor = oldNoColor
316-
}()
287+
ctx := cmdio.MockDiscard(t.Context())
317288

318289
// Create a mock bundle with various resources
319290
b := &bundle.Bundle{

bundle/run/job.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/databricks/cli/libs/cmdio"
1616
"github.com/databricks/cli/libs/log"
1717
"github.com/databricks/databricks-sdk-go/service/jobs"
18-
"github.com/fatih/color"
1918
"github.com/spf13/cobra"
2019
"golang.org/x/sync/errgroup"
2120
)
@@ -50,9 +49,6 @@ func isSuccess(task jobs.RunTask) bool {
5049

5150
func (r *jobRunner) logFailedTasks(ctx context.Context, runId int64) {
5251
w := r.bundle.WorkspaceClient(ctx)
53-
red := color.New(color.FgRed).SprintFunc()
54-
green := color.New(color.FgGreen).SprintFunc()
55-
yellow := color.New(color.FgYellow).SprintFunc()
5652
run, err := w.Jobs.GetRun(ctx, jobs.GetRunRequest{
5753
RunId: runId,
5854
})
@@ -65,21 +61,21 @@ func (r *jobRunner) logFailedTasks(ctx context.Context, runId int64) {
6561
}
6662
for _, task := range run.Tasks {
6763
if isSuccess(task) {
68-
log.Infof(ctx, "task %s completed successfully", green(task.TaskKey))
64+
log.Infof(ctx, "task %s completed successfully", cmdio.Green(ctx, task.TaskKey))
6965
} else if isFailed(task) {
7066
taskInfo, err := w.Jobs.GetRunOutput(ctx, jobs.GetRunOutputRequest{
7167
RunId: task.RunId,
7268
})
7369
if err != nil {
74-
log.Errorf(ctx, "task %s failed. Unable to fetch error trace: %s", red(task.TaskKey), err)
70+
log.Errorf(ctx, "task %s failed. Unable to fetch error trace: %s", cmdio.Red(ctx, task.TaskKey), err)
7571
continue
7672
}
7773
cmdio.Log(ctx, progress.NewTaskErrorEvent(task.TaskKey, taskInfo.Error, taskInfo.ErrorTrace))
7874
log.Errorf(ctx, "Task %s failed!\nError:\n%s\nTrace:\n%s",
79-
red(task.TaskKey), taskInfo.Error, taskInfo.ErrorTrace)
75+
cmdio.Red(ctx, task.TaskKey), taskInfo.Error, taskInfo.ErrorTrace)
8076
} else {
8177
log.Infof(ctx, "task %s is in state %s",
82-
yellow(task.TaskKey), task.State.LifeCycleState)
78+
cmdio.Yellow(ctx, task.TaskKey), task.State.LifeCycleState)
8379
}
8480
}
8581
}

cmd/labs/project/fetcher.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
"strings"
1010

1111
"github.com/databricks/cli/cmd/labs/github"
12+
"github.com/databricks/cli/libs/cmdio"
1213
"github.com/databricks/cli/libs/log"
13-
"github.com/fatih/color"
1414
"github.com/spf13/cobra"
1515
)
1616

@@ -64,7 +64,7 @@ func NewInstaller(cmd *cobra.Command, name string, offlineInstall bool) (install
6464
if err != nil {
6565
return nil, fmt.Errorf("load: %w", err)
6666
}
67-
cmd.PrintErrln(color.YellowString("Installing %s in development mode from %s", prj.Name, wd))
67+
cmd.PrintErrln(cmdio.Yellow(cmd.Context(), fmt.Sprintf("Installing %s in development mode from %s", prj.Name, wd)))
6868
return &devInstallation{
6969
Project: prj,
7070
Command: cmd,
@@ -141,7 +141,7 @@ func (f *fetcher) checkReleasedVersions(cmd *cobra.Command, version string, offl
141141
log.Debugf(ctx, "Latest %s version is: %s", f.name, versions[0].Version)
142142
return versions[0].Version, nil
143143
}
144-
cmd.PrintErrln(color.YellowString("[WARNING] Installing unreleased version: %s", version))
144+
cmd.PrintErrln(cmdio.Yellow(ctx, "[WARNING] Installing unreleased version: "+version))
145145
return version, nil
146146
}
147147

cmd/labs/project/installer.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/databricks/databricks-sdk-go/config"
2121
"github.com/databricks/databricks-sdk-go/service/compute"
2222
"github.com/databricks/databricks-sdk-go/service/sql"
23-
"github.com/fatih/color"
2423
"github.com/spf13/cobra"
2524
)
2625

@@ -152,8 +151,8 @@ func (i *installer) Upgrade(ctx context.Context) error {
152151
return nil
153152
}
154153

155-
func (i *installer) warningf(text string, v ...any) {
156-
i.cmd.PrintErrln(color.YellowString(text, v...))
154+
func (i *installer) warning(s string) {
155+
i.cmd.PrintErrln(cmdio.Yellow(i.cmd.Context(), s))
157156
}
158157

159158
func (i *installer) cleanupLib(ctx context.Context) error {
@@ -288,7 +287,7 @@ func (i *installer) installPythonDependencies(ctx context.Context, spec string)
288287
process.WithCombinedOutput(&buf),
289288
process.WithDir(libDir))
290289
if err != nil {
291-
i.warningf(buf.String())
290+
i.warning(buf.String())
292291
return fmt.Errorf("failed to install dependencies of %s", spec)
293292
}
294293
return nil

cmd/labs/project/project.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
"time"
1212

1313
"github.com/databricks/cli/cmd/labs/github"
14+
"github.com/databricks/cli/libs/cmdio"
1415
"github.com/databricks/cli/libs/env"
1516
"github.com/databricks/cli/libs/log"
1617
"github.com/databricks/cli/libs/python"
1718
"github.com/databricks/databricks-sdk-go/logger"
18-
"github.com/fatih/color"
1919
"go.yaml.in/yaml/v3"
2020

2121
"github.com/spf13/cobra"
@@ -318,7 +318,7 @@ func (p *Project) checkUpdates(cmd *cobra.Command) error {
318318
}
319319
ago := time.Since(latest.PublishedAt)
320320
msg := "[UPGRADE ADVISED] Newer %s version was released %s ago. Please run `databricks labs upgrade %s` to upgrade: %s -> %s"
321-
cmd.PrintErrln(color.YellowString(msg, p.Name, p.timeAgo(ago), p.Name, installed.Version, latest.Version))
321+
cmd.PrintErrln(cmdio.Yellow(ctx, fmt.Sprintf(msg, p.Name, p.timeAgo(ago), p.Name, installed.Version, latest.Version)))
322322
return nil
323323
}
324324

experimental/aitools/cmd/install.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/databricks/cli/experimental/aitools/lib/agents"
1010
"github.com/databricks/cli/experimental/aitools/lib/installer"
1111
"github.com/databricks/cli/libs/cmdio"
12-
"github.com/fatih/color"
1312
"github.com/spf13/cobra"
1413
)
1514

@@ -141,7 +140,7 @@ func filterProjectScopeAgents(detected []*agents.Agent) []*agents.Agent {
141140

142141
// printNoAgentsMessage prints the "no agents detected" message.
143142
func printNoAgentsMessage(ctx context.Context) {
144-
cmdio.LogString(ctx, color.YellowString("No supported coding agents detected."))
143+
cmdio.LogString(ctx, cmdio.Yellow(ctx, "No supported coding agents detected."))
145144
cmdio.LogString(ctx, "")
146145
cmdio.LogString(ctx, "Supported agents: Claude Code, Cursor, Codex CLI, OpenCode, GitHub Copilot, Antigravity")
147146
cmdio.LogString(ctx, "Please install at least one coding agent first.")

0 commit comments

Comments
 (0)