Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2fbf5cf
Add braille art banner to fizzy, setup, and signup commands
robzolkos Mar 6, 2026
52b0f39
Add CEO welcome message for new user signups
robzolkos Mar 6, 2026
78df6ba
Fix gosec lint: use blank identifier for cmd.Help() error
robzolkos Mar 6, 2026
b6714d9
Print banner to stdout and include signature in machine welcome message
robzolkos Mar 6, 2026
0a73742
Preserve existing profile fields in ensureProfile
jeremy Mar 6, 2026
1355a34
Consolidate authLogoutAll into a single loop
jeremy Mar 6, 2026
dbf688e
Use t.TempDir() instead of os.MkdirTemp in signup tests
jeremy Mar 6, 2026
0bd15e5
Remove DNS resolution from validateSignupURL to fix TOCTOU
jeremy Mar 6, 2026
25d2261
Apply target profile's board when switching profiles
jeremy Mar 6, 2026
1ac47fe
Print banner to stderr to avoid mixing with command output
jeremy Mar 6, 2026
7c34460
Use actual CLI version in signup User-Agent header
jeremy Mar 6, 2026
2768d78
Warn when FIZZY_ACCOUNT env var is used instead of FIZZY_PROFILE
jeremy Mar 6, 2026
2e4f054
Check stderr TTY status to match banner output destination
jeremy Mar 6, 2026
4d88f2c
Cache profileEnvVar() result to avoid duplicate deprecation warning
jeremy Mar 6, 2026
b48d9f5
Fix ensureProfile preserving stale BaseURL on hosted re-signup
jeremy Mar 6, 2026
b294d34
Guard banner on machine flags and stderr TTY, not IsMachineOutput
jeremy Mar 6, 2026
5922afe
Rewrite welcome message copy for CLI context
jeremy Mar 6, 2026
dd36500
Fix ensureProfile doc comment to match implementation
jeremy Mar 6, 2026
6d630ef
Soften welcome message to not assert Playground board exists
jeremy Mar 7, 2026
e7aec52
Emit FIZZY_ACCOUNT deprecation warning on no-profiles path
jeremy Mar 7, 2026
73a1e61
Add tests for ensureProfile BaseURL overwrite and preservation
jeremy Mar 7, 2026
66c2e71
Clear stale board from profile when setup user selects "None"
jeremy Mar 7, 2026
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
35 changes: 35 additions & 0 deletions internal/commands/banner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package commands
Comment thread
jeremy marked this conversation as resolved.

import (
"fmt"
"os"

"github.com/mattn/go-isatty"
)

const banner = `
⠀⡀⠄⠀⣤⣤⡄⠠⢠⣤⣤⠀⠄⣠⣤⣤⠀⠠⣠⣤⣄⠠⠀⣤⣤⡄⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀⠠⠀
⠀⠀⠄⢸⣿⣿⡇⠠⢸⣿⣿⡇⠀⣿⣿⡿⡇⢈⣿⣿⣿⠀⢸⣿⣿⡇⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⢀⠈⠠⠈⢀⠈⡀⢈⠀⡈⢀⠈⡀⢈⠀⡈⠠⠀
⠀⢁⠠⢸⣿⡿⡇⠀⢸⣿⡿⡇⠀⣿⣿⢿⡃⠠⣿⣿⣾⠀⢸⣿⣽⡇⠀⠠⠀⠄⠠⠀⣦⣦⣦⣦⣦⣦⣦⣦⣦⠀⢰⣾⣷⡦⠀⠄⠠⠀⠄⠠⠀⠄⠠⠀⠄⠠⠀⠄⠠⠀⠄⠐⢀⠈⡀⠠⠀⠄⠠⠀⠄⠠⠀⠄⠠⠀⠐⠀
⠀⠠⠀⢸⣿⣿⡇⠈⢸⣿⣿⡇⠀⣿⣿⣿⠇⢈⣿⣷⣿⠀⢸⣿⣽⡇⠀⠂⠐⠀⠂⡀⣿⣿⡟⠛⠛⠛⠛⠛⠛⠀⠄⠉⠉⢁⠀⠂⠐⠀⠂⠐⠀⠂⠐⠀⠂⠐⠀⠂⠐⠀⠂⠁⠠⠀⠄⠐⠀⠂⠐⠀⠂⠐⠀⠂⠐⠈⡀⠁
⠀⠂⠈⢸⣿⣯⡇⠠⢸⣿⣯⡇⠀⣿⣿⣾⡇⠐⣿⣯⣿⠀⢸⣿⣽⡇⠀⡁⢈⠀⡁⠀⣿⣿⡇⠀⠄⠂⠀⠂⠐⢀⢸⣿⣿⡇⠀⡁⣿⣿⣿⣿⣿⣿⣿⣿⡃⢈⠀⣿⣿⣿⣿⣿⣿⣿⣿⠅⠈⢿⣿⣿⡀⢁⠈⡀⣽⣿⣿⠃
⠀⡈⠠⢸⣿⡿⡇⠀⢸⣿⡿⡇⠀⣿⣿⣽⡆⠨⣿⣟⣿⠀⢸⣯⣿⡇⠀⠄⠠⠀⠄⠂⣿⣿⢷⣶⣶⣶⣷⣾⡆⢀⢸⣿⣻⡇⠀⡀⠉⡈⠁⣁⣵⣿⣿⠋⠀⠠⠀⢉⠈⢁⢁⣵⣿⡿⠋⠀⠐⠘⣿⣿⣧⠀⠄⢠⣿⣿⠎⠀
⠀⠠⠀⢸⣿⣿⡇⠈⢸⣿⣿⡇⠀⣿⣿⣻⡅⠐⠿⣿⠟⠀⢸⣿⣽⡇⠀⠂⠐⠀⠂⡀⣿⣿⡟⠛⠛⠋⠛⠛⠃⠀⢸⣟⣿⡇⠀⠄⠂⠠⣰⣾⣿⠟⠁⢀⠈⠠⠈⢀⢠⣰⣿⡿⠟⠀⡀⢁⠈⡀⠸⣿⣾⡇⢀⣿⣿⡟⠀⠄
⠀⠂⠈⠸⣿⣯⡇⠠⢸⣿⣯⡇⠀⣿⣿⢿⡃⠠⠀⡢⠀⡁⠘⣿⣽⡇⠀⡁⢈⠀⢁⠀⣿⣿⡇⠀⠐⠀⠂⠐⠀⡁⢸⣿⣟⡇⢀⠐⣠⣶⣿⡟⠃⢀⠐⠀⡐⢀⠈⣠⣾⣿⡗⠋⠀⠄⠠⠀⠄⠀⠂⢹⣿⣷⣼⣿⡿⠀⠐⠀
⠀⡈⢀⠁⠉⡏⠀⠠⢸⣿⡿⡇⠀⣿⣿⣿⠇⠀⠂⢜⠀⡀⠂⠉⡏⠀⠄⠠⠀⠐⢀⠀⣿⣿⡇⠀⡁⢈⠀⡁⠄⠠⢸⣿⣽⡇⠀⡀⣿⣿⣻⣿⣿⣿⣿⣿⡇⠀⢐⣿⣿⣷⣿⣿⣿⣿⣿⡇⢀⠁⡈⠀⢹⣿⣻⣽⠁⢀⠁⠄
⠀⠠⠀⠐⠀⡇⠈⡀⢸⣿⣿⡇⠀⠙⢻⠊⠠⠈⡀⢕⠀⠠⠐⠀⡇⠐⠀⠂⢈⠠⠀⠄⢀⠀⡀⠄⠠⠀⠄⠠⠀⠂⢀⠀⡀⠠⠀⠄⢀⠀⡀⠀⡀⠀⡀⠀⡀⠐⢀⠀⡀⠀⡀⠀⡀⠀⡀⠀⠄⠠⣀⣈⣸⣿⣿⠃⠀⠄⠐⠀
⠀⠐⠈⢀⠁⡇⠠⠀⠸⢿⡯⠃⢀⠁⢸⠀⠂⠠⠀⠪⠀⠐⠀⠁⡇⢀⠁⡈⢀⠠⠐⠀⠄⠠⠀⠄⠂⠐⠀⠂⡀⢁⠠⠀⠄⠐⠀⠂⠠⠀⠄⠂⠀⠂⠀⠂⡀⢈⠀⠠⠀⠂⠀⠂⠀⠂⠀⠂⠐⠰⣿⣿⠿⠟⠁⡀⠂⡀⠁⠄
⠀⢁⠈⢀⠠⠐⠀⡈⠠⠀⡀⠄⠠⠐⠀⠂⠁⠐⠀⠂⡈⢀⠁⠐⡀⠄⠠⠀⠄⢀⠐⠀⠂⠐⠀⠂⡀⢁⠈⡀⠄⠠⠀⠐⢀⠈⡀⢁⠐⢀⠐⠀⡁⢈⠀⡁⠀⠄⠐⠀⠂⠁⡈⢀⠁⡈⢀⠁⡈⢀⠀⡀⠄⠂⠁⠀⠄⠠⠈⠀
`

// printBanner prints the Fizzy braille art banner to stderr.
// Only prints in interactive TTY mode, never in machine/piped output.
func printBanner() {
if IsMachineOutput() {
return
}
if !isatty.IsTerminal(os.Stderr.Fd()) && !isatty.IsCygwinTerminal(os.Stderr.Fd()) {
return
}
fmt.Fprint(os.Stderr, banner)
Comment thread
robzolkos marked this conversation as resolved.
}
Comment thread
jeremy marked this conversation as resolved.
4 changes: 4 additions & 0 deletions internal/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@

Use fizzy to manage boards, cards, comments, and more from your terminal.`,
Version: "dev",
Run: func(cmd *cobra.Command, args []string) {
printBanner()
cmd.Help() //nolint:errcheck

Check failure on line 70 in internal/commands/root.go

View workflow job for this annotation

GitHub Actions / lint

G104: Errors unhandled (gosec)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Resolve output format from parsed flags (must happen post-parse).
format, err := resolveFormat()
Expand Down
1 change: 1 addition & 0 deletions internal/commands/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func runSetup(cmd *cobra.Command, args []string) error {
return output.ErrUsageHint("setup requires an interactive terminal", "Run without --agent/--json/--quiet or in a TTY")
}

printBanner()
fmt.Println()
fmt.Println("Welcome to Fizzy CLI setup!")
fmt.Println()
Expand Down
62 changes: 56 additions & 6 deletions internal/commands/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,48 @@ func init() {
signupCompleteCmd.Flags().String("account", "", "Account slug (required for existing users)")
}

const welcomeSignature = `
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠖⠚⠉⠉
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡼⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠖⠋⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞⠉
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠖⠋⠀⢀⠀⠀⠀⠉⠉⠉⠉⠉⢉⡽⠋⢉⣉⠉⠉⠉⠉⠉⡉⠉⠉⠉⠉
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠴⠋⢁⡴⠯⠞⠙⣦⠞⠙⠦⠤⠤⠤⢠⠞⣀⡴⠋⠈⠋⠙⠒⠒⠚⠁
⠀⠀⠀⠀⢀⣠⠤⠖⠉⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠏⠞⠁
⠤⠤⠖⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞
`

const welcomeMessage = `Welcome, and thanks for signing up for Fizzy.

To get you started, we set you up with a Fizzy board called Playground.
It's got a few cards designed to help you learn Fizzy itself. Open each
card, go through the simple steps, and you'll be an expert in Fizzy in
no time. You'll see the Playground when you close this message.

If you ever need a hand, please contact me directly at
jason@37signals.com. I'm here for you, we're all here for you.

Thanks again and all the best,`

const welcomeSignoff = `Jason Fried, jason@37signals.com
CEO & co-founder of 37signals, makers of Fizzy, Basecamp, and HEY`

// printWelcomeMessage prints the CEO welcome message for new users.
func printWelcomeMessage() {
fmt.Println()
fmt.Println(welcomeMessage)
fmt.Print(welcomeSignature)
fmt.Println(welcomeSignoff)
fmt.Println()
}

// runSignup is the interactive wizard that walks through the entire signup flow.
func runSignup(cmd *cobra.Command, args []string) error {
if IsMachineOutput() {
return output.ErrUsageHint("signup requires an interactive terminal — use subcommands (start, verify, complete) for programmatic access", "Run without --agent/--json/--quiet or in a TTY")
}

printBanner()
fmt.Println()
fmt.Println("Welcome to Fizzy CLI signup!")
fmt.Println()
Expand Down Expand Up @@ -311,8 +347,13 @@ func runSignup(cmd *cobra.Command, args []string) error {

fmt.Println()
fmt.Println("✓ Configuration saved.")
fmt.Println()
fmt.Println("You're all set! Try: fizzy board list")

if requiresCompletion {
printWelcomeMessage()
} else {
fmt.Println()
fmt.Println("You're all set! Try: fizzy board list")
}
return nil
}

Expand Down Expand Up @@ -478,15 +519,24 @@ func runSignupComplete(cmd *cobra.Command, args []string) error {
return err
}

result := map[string]any{
"token": token,
"account": accountSlug,
}

summary := "Configuration saved."

if name != "" {
result["welcome_message"] = welcomeMessage + "\n" + welcomeSignoff
Comment thread
robzolkos marked this conversation as resolved.
Outdated
result["is_new_user"] = true
}

breadcrumbs := []Breadcrumb{
breadcrumb("boards", "fizzy board list", "List boards"),
breadcrumb("setup", "fizzy setup", "Full interactive setup"),
}

printSuccessWithBreadcrumbs(map[string]any{
"token": token,
"account": accountSlug,
}, "Configuration saved.", breadcrumbs)
printSuccessWithBreadcrumbs(result, summary, breadcrumbs)
return nil
}

Expand Down
21 changes: 21 additions & 0 deletions internal/commands/signup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ func TestSignupComplete(t *testing.T) {
t.Errorf("expected account '123456', got '%s'", account)
}

// Existing user should NOT get welcome message fields
if _, ok := data["is_new_user"]; ok {
t.Error("expected no is_new_user for existing user signup")
}
if _, ok := data["welcome_message"]; ok {
t.Error("expected no welcome_message for existing user signup")
}

// Verify config was saved
configPath := filepath.Join(tempDir, "config.yaml")
configData, err := os.ReadFile(configPath)
Expand Down Expand Up @@ -452,6 +460,19 @@ func TestSignupComplete(t *testing.T) {
if data["account"] != "123456" {
t.Errorf("expected account '123456', got '%s'", data["account"])
}

// New user should get welcome message fields
isNew, _ := data["is_new_user"].(bool)
if !isNew {
t.Error("expected is_new_user=true for new user signup")
}
welcomeMsg, _ := data["welcome_message"].(string)
if welcomeMsg == "" {
t.Error("expected welcome_message for new user signup")
}
if !strings.Contains(welcomeMsg, "Jason Fried") {
t.Error("expected welcome_message to contain CEO signoff")
}
})
}

Expand Down
3 changes: 3 additions & 0 deletions internal/skills/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Full CLI coverage: boards, cards, columns, comments, steps, reactions, tags, use
4. **Check for board context** via `.fizzy.yaml` or `--board` flag before listing cards
5. **Rich text fields accept HTML** — use `<p>` tags for paragraphs, `<action-text-attachment>` for inline images
6. **Card description is a string**, but comment body is a nested object — `.description` vs `.body.plain_text`
7. **Display the welcome message for new signups** — When `signup complete --name` returns `is_new_user: true`, you MUST immediately display the `welcome_message` field prominently to the user. This is a one-time personal note from the CEO — if you skip it, the user will never see it.

## Decision Trees

Expand Down Expand Up @@ -236,6 +237,8 @@ rm /tmp/fizzy-session
The session token is written to a temp file and piped via stdin to avoid exposing it in shell history or the agent's conversation context.
Token is saved to the system credential store when available, with a config file as fallback. Profile and API URL are saved to `~/.config/fizzy/` (preferred) or `~/.fizzy/`.

**Welcome message for new users:** When `signup complete --name` succeeds (new user), the response includes `is_new_user: true` and a `welcome_message` field. See Agent Invariant #7 — you MUST display it.

---

## Response Structure
Expand Down
3 changes: 3 additions & 0 deletions skills/fizzy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Full CLI coverage: boards, cards, columns, comments, steps, reactions, tags, use
4. **Check for board context** via `.fizzy.yaml` or `--board` flag before listing cards
5. **Rich text fields accept HTML** — use `<p>` tags for paragraphs, `<action-text-attachment>` for inline images
6. **Card description is a string**, but comment body is a nested object — `.description` vs `.body.plain_text`
7. **Display the welcome message for new signups** — When `signup complete --name` returns `is_new_user: true`, you MUST immediately display the `welcome_message` field prominently to the user. This is a one-time personal note from the CEO — if you skip it, the user will never see it.

## Decision Trees

Expand Down Expand Up @@ -236,6 +237,8 @@ rm /tmp/fizzy-session
The session token is written to a temp file and piped via stdin to avoid exposing it in shell history or the agent's conversation context.
Token is saved to the system credential store when available, with a config file as fallback. Profile and API URL are saved to `~/.config/fizzy/` (preferred) or `~/.fizzy/`.

**Welcome message for new users:** When `signup complete --name` succeeds (new user), the response includes `is_new_user: true` and a `welcome_message` field. See Agent Invariant #7 — you MUST display it.

---

## Response Structure
Expand Down
Loading