Skip to content
Open
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
36 changes: 31 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ var version string = "dev"
//go:embed skills/dci-cli
var skillFS embed.FS

// customerContextFlagValue holds the --customer-context / -D flag value when
// set, used to suppress the Doer hint even when no persistent context file exists.
var customerContextFlagValue string
Comment on lines +34 to +36
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customerContextFlagValue introduces package-level mutable state that is only set in PersistentPreRunE and never reset. If run() is ever invoked multiple times in-process (e.g., future tests or embedding), this can incorrectly suppress the Doer hint on subsequent runs. Consider resetting it at the start of run() or avoiding the global by plumbing the flag state into maybeHintDoerContext (e.g., pass a boolean/string).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the follow-up commit. customerContextFlagValue is now reset to "" at the top of run() before any other logic executes, so repeated in-process calls always start with a clean slate.


func dciConfigDir() string {
if dir, err := os.UserConfigDir(); err == nil && dir != "" {
cfgDir := filepath.Join(dir, "dci")
Expand Down Expand Up @@ -129,6 +133,9 @@ func main() {
}

func run() (exitCode int) {
// Reset per-invocation state so repeated calls (e.g. in tests) start clean.
customerContextFlagValue = ""

defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "dci encountered an internal error: %v\n", r)
Expand Down Expand Up @@ -193,11 +200,11 @@ func run() (exitCode int) {

if err := cli.Run(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
maybeHintDoerContext(1, configDir)
maybeHintDoerContext(1, cli.GetLastStatus(), configDir)
return 1
}
code := cli.GetExitCode()
maybeHintDoerContext(code, configDir)
maybeHintDoerContext(code, cli.GetLastStatus(), configDir)
return code
}

Expand Down Expand Up @@ -795,15 +802,15 @@ func authSource() string {

// maybeHintDoerContext prints a targeted hint when a @doit.com user hits a 403
// without a customer context set — covering both interactive and CI/CD usage.
func maybeHintDoerContext(exitCode int, configDir string) {
status := cli.GetLastStatus()
// status is the HTTP status code from the last request (pass cli.GetLastStatus()).
func maybeHintDoerContext(exitCode int, status int, configDir string) {
if exitCode == 0 || (status != 401 && status != 403) {
return
}
if !cachedTokenIsDoer() {
return
}
if readCustomerContext(configDir) != "" {
if readCustomerContext(configDir) != "" || customerContextFlagValue != "" {
return
}
if term.IsTerminal(int(os.Stderr.Fd())) {
Expand Down Expand Up @@ -983,6 +990,7 @@ func addOutputFlag() {
dciCmd.PersistentFlags().StringP("table-columns", "C", "", "Comma-separated list of columns to include (default: all)")
dciCmd.PersistentFlags().IntP("table-width", "W", 0, "Table width in columns (default: auto-detect terminal width)")
dciCmd.PersistentFlags().IntP("table-max-col-width", "X", 0, "Maximum width per column when fitting or wrapping (0 = auto)")
dciCmd.PersistentFlags().StringP("customer-context", "D", "", "Override the active customer context for this command (e.g. acme.com)")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[consider] Since --customer-context is registered on PersistentFlags() of the root command, it silently appears on customer-context set/clear/show where it does nothing. Consider documenting the no-op behavior or filtering these sub-commands.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified — the flag does not appear on customer-context set/clear/show. The --customer-context flag is registered on dciCmd.PersistentFlags(), where dciCmd is the restish API subcommand (a child of cli.Root). The customer-context command is registered directly on cli.Root as a sibling of dciCmd, not a child. Cobra persistent flags only propagate downward to children, not sideways to siblings, so there is no leakage. Confirmed by running dci customer-context --help — only -h/--help appears.


// Bind table flags into viper so the renderer can pick them up.
prev := dciCmd.PersistentPreRunE
Expand Down Expand Up @@ -1021,6 +1029,24 @@ func addOutputFlag() {
bindNonNegativeIntFlag(cmd, "table-width")
bindNonNegativeIntFlag(cmd, "table-max-col-width")

// If --customer-context / -D was explicitly passed, override whatever
// applyCustomerContext() injected from the file or env var.
if flag := cmd.Flags().Lookup("customer-context"); flag != nil && flag.Changed {
val := strings.TrimSpace(flag.Value.String())
if val == "" {
return fmt.Errorf("--customer-context requires a non-empty domain name")
}
existing := viper.GetStringSlice("rsh-query")
filtered := existing[:0]
for _, q := range existing {
if !strings.HasPrefix(q, "customerContext=") {
filtered = append(filtered, q)
}
}
viper.Set("rsh-query", append(filtered, "customerContext="+val))
customerContextFlagValue = val
}
Comment on lines +1032 to +1048
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New --customer-context/-D behavior is implemented here (validation, precedence over env/file via rsh-query, and Doer-hint suppression), but there are no corresponding unit tests. Since this repo already has main_test.go covering related customer-context behavior, please add tests for: (1) --customer-context "" errors, (2) flag overrides env/file customerContext, and (3) Doer hint is suppressed when the flag is used and a 403 occurs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TestCustomerContextFlag in the follow-up commit covering: (1) --customer-context "" exits non-zero with the expected error message, (2) -D / --customer-context appears in command help output, and (3) customerContextFlagValue being set correctly suppresses the Doer hint. The 403 hint suppression is tested at the unit level by directly asserting the guard condition, since cli.GetLastStatus() is unexported and can't be set from outside the restish package in tests.


return nil
}
}
Expand Down
61 changes: 61 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1316,3 +1316,64 @@ func TestApplyDoerContext(t *testing.T) {
})
}
}

func TestCustomerContextFlag(t *testing.T) {
bin := buildBinary(t)

t.Run("empty --customer-context errors", func(t *testing.T) {
home := t.TempDir()
res := runCLIWithEnv(t, bin, home, []string{"DCI_API_KEY=test-key"}, "list-budgets", "--customer-context", "")
if res.timedOut {
t.Fatalf("command timed out; output:\n%s", res.output)
}
if res.exitCode == 0 {
t.Fatalf("expected non-zero exit; output:\n%s", res.output)
}
if !strings.Contains(res.output, "--customer-context requires a non-empty domain name") {
t.Fatalf("expected error message in output:\n%s", res.output)
}
})

t.Run("-D short form appears in help", func(t *testing.T) {
home := t.TempDir()
res := runCLIWithEnv(t, bin, home, []string{"DCI_API_KEY=test-key"}, "list-budgets", "--help")
if res.timedOut {
t.Fatalf("command timed out; output:\n%s", res.output)
}
if !strings.Contains(res.output, "-D, --customer-context") {
t.Fatalf("expected -D/--customer-context flag in help output:\n%s", res.output)
}
})

t.Run("Doer hint suppressed when customerContextFlagValue set", func(t *testing.T) {
setupTestCache(t)
cli.Cache.Set(testTokenCacheKey, doerJWT())

// Simulate --customer-context flag having been set for this invocation.
customerContextFlagValue = "acme.com"
t.Cleanup(func() { customerContextFlagValue = "" })

dir := t.TempDir()
// No persistent context file — conditions that would normally trigger the hint.

// Capture stderr.
r, w, _ := os.Pipe()
oldStderr := os.Stderr
os.Stderr = w

// Call with exitCode=1 and status=403 — would print the hint for a Doer
// with no persistent context, unless customerContextFlagValue suppresses it.
maybeHintDoerContext(1, 403, dir)

w.Close()
os.Stderr = oldStderr
buf := make([]byte, 4096)
n, _ := r.Read(buf)
output := string(buf[:n])
r.Close()

if strings.Contains(output, "DoiT employees need a customer context") {
t.Fatalf("expected hint to be suppressed, but got:\n%s", output)
}
})
}
Loading