Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/internal/cli/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func runConnect(ctx context.Context, commandContext *commandContext, kubeClient
if !connectedBefore || kube.IsNotFound(err) {
return err
}
if _, werr := fmt.Fprintf(commandContext.err, "Reconnect to %s/%s failed: %v; retrying in %s...\n", namespace, name, err, backoff); werr != nil {
if _, werr := fmt.Fprintf(commandContext.err, "Reconnect to %s/%s failed: %v; retrying in %s...\n", namespace, name, err, backoff); werr != nil { // ui-passthrough-ok
return werr
}
select {
Expand Down Expand Up @@ -117,14 +117,14 @@ func runConnect(ctx context.Context, commandContext *commandContext, kubeClient
warnRemoveEndpointsFile(commandContext.err, path)
// Best-effort status: a clean interrupt must not become a failure
// exit code over a stderr write hiccup.
_, _ = fmt.Fprintf(commandContext.err, "Disconnecting from %s/%s.\n", namespace, name)
_, _ = fmt.Fprintf(commandContext.err, "Disconnecting from %s/%s.\n", namespace, name) // ui-passthrough-ok

return nil
case <-connected.Done():
reason := connected.Err()
_ = connected.Close()
warnRemoveEndpointsFile(commandContext.err, path)
if _, err := fmt.Fprintf(commandContext.err, "Forwards to %s/%s dropped (%v); re-establishing...\n", namespace, name, reason); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "Forwards to %s/%s dropped (%v); re-establishing...\n", namespace, name, reason); err != nil { // ui-passthrough-ok
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/devnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func newDevnetDownCommand(commandContext *commandContext) *cobra.Command {
return err
}

_, err = fmt.Fprintln(commandContext.out, "devnet cluster removed.")
_, err = fmt.Fprintln(commandContext.out, "devnet cluster removed.") // ui-passthrough-ok
return err
},
}
Expand Down
4 changes: 2 additions & 2 deletions cli/internal/cli/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func newDownCommand(commandContext *commandContext) *cobra.Command {
return err
}

if _, err := fmt.Fprintf(commandContext.err, "Deleting CardanoNetwork %s/%s...\n", namespace, name); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "Deleting CardanoNetwork %s/%s...\n", namespace, name); err != nil { // ui-passthrough-ok
return fmt.Errorf("write delete status: %w", err)
}
if err := kubeClient.DeleteCardanoNetwork(cmd.Context(), namespace, name); err != nil {
Expand All @@ -54,7 +54,7 @@ func newDownCommand(commandContext *commandContext) *cobra.Command {
if err := kube.WaitGone(cmd.Context(), kubeClient, namespace, name, timeout); err != nil {
return err
}
if _, err := fmt.Fprintf(commandContext.err, "CardanoNetwork %s/%s is gone.\n", namespace, name); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "CardanoNetwork %s/%s is gone.\n", namespace, name); err != nil { // ui-passthrough-ok
return fmt.Errorf("write gone status: %w", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func newInfoCommand(commandContext *commandContext) *cobra.Command {
if err != nil {
return fmt.Errorf("marshal info JSON: %w", err)
}
if _, err := fmt.Fprintf(commandContext.out, "%s\n", encoded); err != nil {
if _, err := fmt.Fprintf(commandContext.out, "%s\n", encoded); err != nil { // ui-passthrough-ok
return fmt.Errorf("write info JSON: %w", err)
}
return nil
Expand Down
6 changes: 3 additions & 3 deletions cli/internal/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func runInstallPlan(ctx context.Context, commandContext *commandContext, install
if installed == "" {
installed = "none"
}
_, err = fmt.Fprintf(commandContext.out, "Plan: %s operator (installed %s -> %s) in namespace %s\n",
_, err = fmt.Fprintf(commandContext.out, "Plan: %s operator (installed %s -> %s) in namespace %s\n", // ui-passthrough-ok
actionVerb(decision.Action), installed, decision.TargetVersion, namespace)
return err
}
Expand Down Expand Up @@ -196,11 +196,11 @@ func runInstall(ctx context.Context, commandContext *commandContext, report life
}

if waitReady {
_, err = fmt.Fprintf(commandContext.out, "Operator %s ready in namespace %s\n", state.Version, namespace)
_, err = fmt.Fprintf(commandContext.out, "Operator %s ready in namespace %s\n", state.Version, namespace) // ui-passthrough-ok
return err
}

_, err = fmt.Fprintf(commandContext.out, "Operator %s installed in namespace %s (not waiting for readiness)\n", state.Version, namespace)
_, err = fmt.Fprintf(commandContext.out, "Operator %s installed in namespace %s (not waiting for readiness)\n", state.Version, namespace) // ui-passthrough-ok
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func newListCommand(commandContext *commandContext) *cobra.Command {
if err != nil {
return fmt.Errorf("marshal list JSON: %w", err)
}
if _, err := fmt.Fprintf(commandContext.out, "%s\n", encoded); err != nil {
if _, err := fmt.Fprintf(commandContext.out, "%s\n", encoded); err != nil { // ui-passthrough-ok
return fmt.Errorf("write list JSON: %w", err)
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/orphan.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ func (commandContext *commandContext) clearManagedStateRecord() {
if err := store.Clear(); err != nil {
return
}
_, _ = fmt.Fprintln(commandContext.err, orphanedStateMessage)
_, _ = fmt.Fprintln(commandContext.err, orphanedStateMessage) // ui-passthrough-ok
}
86 changes: 86 additions & 0 deletions cli/internal/cli/purity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cli

import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

// rawStreamWrite matches the banned process/raw-stream write forms: any
// os.Std*, any writer-less fmt.Print family, lipgloss.Print*, and any fmt.Fprint
// family targeting the raw commandContext.out/err streams. Helper functions that
// take an io.Writer parameter are intentionally not matched — they receive
// cc.io.Data() once routing lands. Data must reach stdout only through
// cc.io.Data/Encode; the human plane only through cc.io.* helpers.
var rawStreamWrite = regexp.MustCompile(
`os\.Std(out|err|in)\b` +
`|fmt\.Print(f|ln)?\(` +
`|lipgloss\.Print` +
`|fmt\.Fprint(f|ln)?\(\s*commandContext\.(out|err)\b`,
)

// passthroughPragma is the line-level escape hatch for the run/exec
// byte-transparent passthrough and the wallet_fund os.Stdout swap, plus the
// interim raw writes that later routing phases move onto cc.io. It is a per-line
// pragma (written "// ui-passthrough-ok"), never a whole-file allowlist; the
// bare token is matched so comment spacing does not matter.
const passthroughPragma = "ui-passthrough-ok"

func TestNoRawStreamWrites(t *testing.T) {
t.Parallel()

root := purityModuleRoot(t)
dir := filepath.Join(root, "cli", "internal", "cli")

err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for n, line := range strings.Split(string(data), "\n") {
// Skip full comment lines so prose mentioning os.Stdout / fmt.Print
// does not trip the guard; trailing-comment pragmas on real write
// lines are still honored below.
if strings.HasPrefix(strings.TrimSpace(line), "//") {
continue
}
if !rawStreamWrite.MatchString(line) || strings.Contains(line, passthroughPragma) {
continue
}
rel, _ := filepath.Rel(root, path)
t.Errorf("%s:%d raw stream write; route through cc.io (or tag // %s for passthrough): %s",
rel, n+1, passthroughPragma, strings.TrimSpace(line))
}
return nil
})
if err != nil {
t.Fatal(err)
}
}

// purityModuleRoot walks up from the working directory to the first go.mod.
func purityModuleRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for {
if _, statErr := os.Stat(filepath.Join(dir, "go.mod")); statErr == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find module root")
}
dir = parent
}
}
2 changes: 1 addition & 1 deletion cli/internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ to the command instead of being parsed by yacd.`,
_ = connected.Close()
}()

if _, err := fmt.Fprintf(commandContext.err, "Connected to %s/%s; running %s\n", namespace, name, strings.Join(command, " ")); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "Connected to %s/%s; running %s\n", namespace, name, strings.Join(command, " ")); err != nil { // ui-passthrough-ok
return fmt.Errorf("write run status: %w", err)
}

Expand Down
10 changes: 5 additions & 5 deletions cli/internal/cli/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ func newUpCommand(commandContext *commandContext) *cobra.Command {
return fmt.Errorf("write manifest: %w", err)
}
if len(manifest) == 0 || manifest[len(manifest)-1] != '\n' {
if _, err := fmt.Fprintln(commandContext.out); err != nil {
if _, err := fmt.Fprintln(commandContext.out); err != nil { // ui-passthrough-ok
return fmt.Errorf("write manifest newline: %w", err)
}
}
_, err = fmt.Fprintf(commandContext.err, "Dry run: rendered CardanoNetwork %s/%s; no resources applied.\n", network.Namespace, network.Name)
_, err = fmt.Fprintf(commandContext.err, "Dry run: rendered CardanoNetwork %s/%s; no resources applied.\n", network.Namespace, network.Name) // ui-passthrough-ok
return err
}

Expand All @@ -100,18 +100,18 @@ func newUpCommand(commandContext *commandContext) *cobra.Command {
return err
}
commandContext.logger.Debug("Applied CardanoNetwork", "namespace", network.Namespace, "name", network.Name)
if _, err := fmt.Fprintf(commandContext.err, "Applied CardanoNetwork %s/%s.\n", network.Namespace, network.Name); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "Applied CardanoNetwork %s/%s.\n", network.Namespace, network.Name); err != nil { // ui-passthrough-ok
return fmt.Errorf("write apply status: %w", err)
}

if waitReady {
if _, err := fmt.Fprintf(commandContext.err, "Waiting for CardanoNetwork %s/%s to become ready...\n", network.Namespace, network.Name); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "Waiting for CardanoNetwork %s/%s to become ready...\n", network.Namespace, network.Name); err != nil { // ui-passthrough-ok
return fmt.Errorf("write wait status: %w", err)
}
if _, err := kube.WaitReady(cmd.Context(), kubeClient, network.Namespace, network.Name, timeout); err != nil {
return err
}
if _, err := fmt.Fprintf(commandContext.err, "CardanoNetwork %s/%s is ready.\n", network.Namespace, network.Name); err != nil {
if _, err := fmt.Fprintf(commandContext.err, "CardanoNetwork %s/%s is ready.\n", network.Namespace, network.Name); err != nil { // ui-passthrough-ok
return fmt.Errorf("write ready status: %w", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func newVersionCommand(commandContext *commandContext) *cobra.Command {
Short: "Print the yacd version",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
_, err := fmt.Fprintf(commandContext.out, "yacd %s (%s) built %s\n",
_, err := fmt.Fprintf(commandContext.out, "yacd %s (%s) built %s\n", // ui-passthrough-ok
commandContext.build.Version, commandContext.build.Commit, commandContext.build.Date)
return err
},
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/cli/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func newWalletRemoveCommand(commandContext *commandContext) *cobra.Command {
return err
}

_, err = fmt.Fprintf(commandContext.out, "Removed wallet %q from %s/%s.\n", name, walletCtx.namespace, walletCtx.name)
_, err = fmt.Fprintf(commandContext.out, "Removed wallet %q from %s/%s.\n", name, walletCtx.namespace, walletCtx.name) // ui-passthrough-ok
return err
},
}
Expand Down
8 changes: 4 additions & 4 deletions cli/internal/cli/wallet_fund.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (commandContext *commandContext) fundWallet(
// One-time notice to stderr so the otherwise-silent poll does not look
// hung; it stays off stdout to keep --json output clean and is
// best-effort because the transfer is already submitted.
_, _ = fmt.Fprintf(commandContext.err, "Waiting up to %s for %s to confirm on-chain...\n", request.awaitTimeout, result.TxID)
_, _ = fmt.Fprintf(commandContext.err, "Waiting up to %s for %s to confirm on-chain...\n", request.awaitTimeout, result.TxID) // ui-passthrough-ok
if err := awaitConfirmation(ctx, confirmer, request.destinationAddress, result.TxID, request.awaitTimeout); err != nil {
return fundResult{}, err
}
Expand Down Expand Up @@ -191,8 +191,8 @@ func sourceKeyMaterial(ctx context.Context, kubeClient kube.Client, namespace st
// which retain the original streams, so its own output is unaffected. It is
// safe because the CLI runs a single command synchronously.
func redirectStdoutToStderr() func() {
original := os.Stdout
os.Stdout = os.Stderr
original := os.Stdout // ui-passthrough-ok
os.Stdout = os.Stderr // ui-passthrough-ok

return func() { os.Stdout = original }
return func() { os.Stdout = original } // ui-passthrough-ok
}
91 changes: 91 additions & 0 deletions cli/internal/ui/guard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ui_test

import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
)

// bannedManagerDeps matches any import path that would prove charm — or another
// CLI-only heavy dependency — leaked into the operator manager's import graph.
// The manager builds from ./cmd and must never import cli/internal/ui (the only
// package allowed to depend on charm), so `go list -deps ./cmd` is a structural
// tripwire rather than a per-PR review burden.
var bannedManagerDeps = regexp.MustCompile(
`charm|lipgloss|huh|bubble|ultraviolet|colorprofile|charmbracelet|ogmigo|kugo|internal/cardano/tx`,
)

func TestManagerImportGraphHasNoCharm(t *testing.T) {
t.Parallel()

root := moduleRoot(t)

cmd := exec.Command("go", "list", "-deps", "./cmd")
cmd.Dir = root
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go list -deps ./cmd failed: %v\n%s", err, out)
}

for line := range strings.SplitSeq(strings.TrimSpace(string(out)), "\n") {
if bannedManagerDeps.MatchString(line) {
t.Errorf("manager import graph contains banned dependency %q; charm and CLI-only deps must stay out of ./cmd", line)
}
}
}

// legacyCharmImport matches the pre-v2 charmbracelet module paths the v2
// migration forbids anywhere under cli/. v2 lives at charm.land/*; importing
// github.com/charmbracelet/(lipgloss|huh|log) would silently pull a v0/v1 API.
var legacyCharmImport = regexp.MustCompile(`github\.com/charmbracelet/(lipgloss|huh|log)`)

func TestNoLegacyCharmPaths(t *testing.T) {
t.Parallel()

root := moduleRoot(t)

err := filepath.WalkDir(filepath.Join(root, "cli"), func(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
for n, line := range strings.Split(string(data), "\n") {
if legacyCharmImport.MatchString(line) {
rel, _ := filepath.Rel(root, path)
t.Errorf("%s:%d imports a legacy charmbracelet path; use charm.land/* v2: %s", rel, n+1, strings.TrimSpace(line))
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}

// moduleRoot walks up from the working directory to the first go.mod.
func moduleRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for {
if _, statErr := os.Stat(filepath.Join(dir, "go.mod")); statErr == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find module root")
}
dir = parent
}
}
Loading