Skip to content

Commit 30de8eb

Browse files
committed
Polish TUI flow and login behavior
1 parent b3a1b3a commit 30de8eb

3 files changed

Lines changed: 141 additions & 48 deletions

File tree

README.md

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -81,49 +81,29 @@ goreleaser check
8181
goreleaser release --snapshot --clean
8282
```
8383

84-
## Implementation Plan
85-
The current fish scripts (`sprite_up`/`sprite_destroy`) define the baseline behavior. The plan below maps that flow into a robust, cross-platform CLI built with Bubble Tea.
86-
87-
### Phase 1: CLI foundations
88-
- Initialize a Go CLI with a single `seven` binary.
89-
- Implement commands:
90-
- `seven up` (create or reuse a sprite, open console)
91-
- `seven destroy` (delete sprite, remove local marker file)
92-
- `seven status` (show sprite existence + health)
93-
- Add a config/marker file (`.sprite`) to pin the sprite name for the repo.
94-
95-
### Phase 2: Sprite lifecycle logic
96-
- Name resolution:
97-
- Default to `basename(pwd)` unless `.sprite` overrides it.
98-
- Validate names and provide helpful errors.
99-
- Create/reuse logic:
100-
- If sprite exists, reuse and open console.
101-
- If not, create with `sprite create --skip-console`.
102-
- Destroy logic:
103-
- Remove local `.sprite` file.
104-
- Destroy sprite if it exists.
105-
106-
### Phase 3: Repo bootstrap
107-
- Detect git repo and `origin` remote.
108-
- If GitHub remote:
109-
- Use `gh auth token` if available for faster clone.
110-
- `gh repo clone <owner/repo> <sprite-name>`.
111-
- Else fallback to `git clone <repo-url> <sprite-name>`.
112-
113-
### Phase 4: TUI experience (Bubble Tea)
114-
- Add a guided flow for `seven up`:
115-
- status spinner, name confirm/override, progress logs.
116-
- Provide success/failure summary with next actions.
117-
- Make it easy to cancel safely (Ctrl+C).
118-
119-
### Phase 5: polish + docs
120-
- Structured logging and clear error messages.
121-
- Extend README with examples, troubleshooting, and FAQ.
122-
- Add tests for name resolution, `.sprite` handling, and command invocation.
123-
- Document installation:
124-
- **Primary:** prebuilt binaries per OS/arch (GitHub Releases).
125-
- **Convenience:** package managers (Homebrew/Scoop/Winget) as optional wrappers.
126-
- **POC focus:** a simple curl-based installer that fetches the correct release asset.
84+
## Implementation Plan (short)
85+
- **Core CLI:** `seven init`, `seven up`, `seven destroy`, `seven status`.
86+
- **Bootstrap:** resolve sprite name, create/reuse sprite, clone repo when possible.
87+
- **TUI:** minimal Bubble Tea flow with progress and clean status output.
88+
- **Packaging:** GitHub Releases + curl installer (primary), package managers later.
89+
90+
## Upcoming: IDE + Networking Experience
91+
### 1) IDE connection (VS Code and others)
92+
Goal: open the sprite workspace in your local IDE without losing native tooling.
93+
94+
Planned approach:
95+
- **Session-first UX:** `seven up` opens a shell via `sprite console`, while `seven exec` (and the CLI’s `sprite exec`) can run commands in the sprite with full TTY support. citeturn1view0
96+
- **IDE adapters:** add a `seven ide` flow that can:
97+
- detect an SSH-capable endpoint if Sprites exposes one,
98+
- or fall back to syncing + remote commands (git-based or file sync) when SSH isn’t available.
99+
100+
### 2) Feedback loop + port forwarding
101+
Goal: run the app inside the sprite and access it locally as if it were running on your machine.
102+
103+
Planned approach:
104+
- **Forwarding:** use `sprite proxy` to forward local ports to the sprite. citeturn1view1
105+
- **Auto-discovery:** detect ports by heuristics (common dev ports, package scripts, Docker compose, `.env`, or a user-specified list).
106+
- **Command surface:** add `seven forward` (and/or `seven dev`) to start the app in the sprite and wire up ports in one step.
127107

128108
## References
129109
- Fly.io Sprite docs: https://docs.sprites.dev/

cmd/seven/main.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func main() {
3838
}
3939

4040
switch os.Args[1] {
41+
case "--version", "-v", "version":
42+
printVersion()
43+
return
4144
case "init":
4245
cmdInit(os.Args[2:])
4346
case "up":
@@ -57,6 +60,7 @@ func main() {
5760

5861
func usage() {
5962
fmt.Println("seven - vagrant-style workflow backed by fly.io sprites")
63+
fmt.Printf("version: %s\n", version)
6064
fmt.Println()
6165
fmt.Println("Usage:")
6266
fmt.Println(" seven init [--assume-logged-in]")
@@ -65,12 +69,19 @@ func usage() {
6569
fmt.Println(" seven status")
6670
fmt.Println()
6771
fmt.Println("Commands:")
72+
fmt.Println(" version Show version")
6873
fmt.Println(" init One-time setup (login, create sprite, clone repo)")
6974
fmt.Println(" up Create or reuse a sprite, bootstrap repo, open console")
7075
fmt.Println(" destroy Destroy the current sprite and remove .sprite file")
7176
fmt.Println(" status Show sprite status for this repo")
7277
}
7378

79+
var version = "dev"
80+
81+
func printVersion() {
82+
fmt.Println(version)
83+
}
84+
7485
func cmdUp(args []string) {
7586
fs := flag.NewFlagSet("up", flag.ExitOnError)
7687
noTUI := fs.Bool("no-tui", false, "disable TUI output")
@@ -293,7 +304,11 @@ func runInit(opts upOptions) (upResult, error) {
293304
}
294305

295306
opts.Logger("[seven init] creating sprite")
296-
if err := runCmdDevNull(spriteBin(), nil, "create", "--skip-console", name); err != nil {
307+
if opts.QuietExternal {
308+
if err := runCmdQuiet(spriteBin(), nil, "create", "--skip-console", name); err != nil {
309+
return upResult{}, err
310+
}
311+
} else if err := runCmdDevNull(spriteBin(), nil, "create", "--skip-console", name); err != nil {
297312
return upResult{}, err
298313
}
299314

@@ -340,6 +355,19 @@ func runInit(opts upOptions) (upResult, error) {
340355
}
341356

342357
func runUpWithTUI(assumeLoggedIn bool, openConsole bool) (upResult, error) {
358+
if !assumeLoggedIn {
359+
if err := ensureSpriteCLI(); err != nil {
360+
return upResult{}, err
361+
}
362+
if _, err := spriteList(); err != nil {
363+
fmt.Println(formatStyledBulletLog("[seven init] logging in to sprite"))
364+
if err := runCmd(spriteBin(), nil, "login"); err != nil {
365+
return upResult{}, err
366+
}
367+
}
368+
assumeLoggedIn = true
369+
}
370+
343371
m := newUpModel()
344372
p := tea.NewProgram(m)
345373

@@ -646,6 +674,22 @@ func runCmdDevNull(name string, extraEnv []string, args ...string) error {
646674
return cmd.Run()
647675
}
648676

677+
func runCmdQuiet(name string, extraEnv []string, args ...string) error {
678+
cmd := exec.Command(name, args...)
679+
cmd.Stdout = nil
680+
cmd.Stderr = nil
681+
devNull, err := os.Open(os.DevNull)
682+
if err != nil {
683+
return err
684+
}
685+
defer devNull.Close()
686+
cmd.Stdin = devNull
687+
if len(extraEnv) > 0 {
688+
cmd.Env = append(os.Environ(), extraEnv...)
689+
}
690+
return cmd.Run()
691+
}
692+
649693
func runCmdOutput(name string, extraEnv []string, args ...string) (string, error) {
650694
cmd := exec.Command(name, args...)
651695
if len(extraEnv) > 0 {
@@ -719,7 +763,6 @@ func (m upModel) View() string {
719763
}
720764
fmt.Fprintf(b, "%s\n", headerStyle.Render(title))
721765
if len(m.logs) > 0 {
722-
fmt.Fprintln(b)
723766
start := 0
724767
if len(m.logs) > 6 {
725768
start = len(m.logs) - 6
@@ -736,9 +779,6 @@ func (m upModel) View() string {
736779
if m.err != nil {
737780
fmt.Fprintf(b, "\n%s %v\n", errorStyle.Render("error:"), m.err)
738781
}
739-
if m.err == nil && m.res.Name != "" {
740-
fmt.Fprintf(b, "\n%s %s\n", bulletStyle.Render("•"), subtleStyle.Render("sprite: "+m.res.Name))
741-
}
742782
return b.String()
743783
}
744784

cmd/seven/main_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,67 @@ func TestSevenUpCreatesSpriteAndWritesFile(t *testing.T) {
9191
}
9292
}
9393

94+
func TestSevenUpSkipsLoginWhenSpriteExists(t *testing.T) {
95+
repo := t.TempDir()
96+
state, logPath, cleanup := createFakeSprite(t)
97+
defer cleanup()
98+
99+
spriteName := "existing-sprite"
100+
if err := os.WriteFile(filepath.Join(repo, ".sprite"), []byte(spriteName+"\n"), 0o644); err != nil {
101+
t.Fatalf("failed to write .sprite: %v", err)
102+
}
103+
if err := os.WriteFile(state, []byte(spriteName+"\n"), 0o644); err != nil {
104+
t.Fatalf("failed to write state: %v", err)
105+
}
106+
107+
cmd := exec.Command(testSevenBin, "up", "--no-tui")
108+
cmd.Dir = repo
109+
cmd.Env = append(os.Environ(),
110+
"PATH="+filepath.Dir(state)+string(os.PathListSeparator)+os.Getenv("PATH"),
111+
"SPRITE_STATE="+state,
112+
"SPRITE_LOG="+logPath,
113+
)
114+
output, err := cmd.CombinedOutput()
115+
if err != nil {
116+
t.Fatalf("seven up failed: %v\n%s", err, output)
117+
}
118+
119+
logData, err := os.ReadFile(logPath)
120+
if err != nil {
121+
t.Fatalf("expected sprite log: %v", err)
122+
}
123+
if strings.Contains(string(logData), "login") {
124+
t.Fatalf("expected no login when sprite exists, got: %s", logData)
125+
}
126+
}
127+
128+
func TestSevenUpLogsInWhenSpriteListFails(t *testing.T) {
129+
repo := createTempRepo(t)
130+
state, logPath, cleanup := createFakeSprite(t)
131+
defer cleanup()
132+
133+
cmd := exec.Command(testSevenBin, "up", "--no-tui")
134+
cmd.Dir = repo
135+
cmd.Env = append(os.Environ(),
136+
"PATH="+filepath.Dir(state)+string(os.PathListSeparator)+os.Getenv("PATH"),
137+
"SPRITE_STATE="+state,
138+
"SPRITE_LOG="+logPath,
139+
"SPRITE_FAIL_LIST=once",
140+
)
141+
output, err := cmd.CombinedOutput()
142+
if err != nil {
143+
t.Fatalf("seven up failed: %v\n%s", err, output)
144+
}
145+
146+
logData, err := os.ReadFile(logPath)
147+
if err != nil {
148+
t.Fatalf("expected sprite log: %v", err)
149+
}
150+
if !strings.Contains(string(logData), "login") {
151+
t.Fatalf("expected login when list fails, got: %s", logData)
152+
}
153+
}
154+
94155
func TestSevenInitSetsUpSpriteWithoutConsole(t *testing.T) {
95156
repo := createTempRepo(t)
96157
state, logPath, cleanup := createFakeSprite(t)
@@ -244,6 +305,18 @@ case "$cmd" in
244305
exit 0
245306
;;
246307
list)
308+
if [ "${SPRITE_FAIL_LIST:-}" = "1" ]; then
309+
logit "list (fail)"
310+
exit 1
311+
fi
312+
if [ "${SPRITE_FAIL_LIST:-}" = "once" ]; then
313+
failflag="${state}.fail_once"
314+
if [ ! -f "$failflag" ]; then
315+
logit "list (fail)"
316+
echo 1 > "$failflag"
317+
exit 1
318+
fi
319+
fi
247320
logit "list"
248321
if [ -f "$state" ]; then
249322
cat "$state"

0 commit comments

Comments
 (0)