Skip to content

Commit ad64eb7

Browse files
committed
feat: add workspace command
1 parent c410af9 commit ad64eb7

File tree

10 files changed

+670
-49
lines changed

10 files changed

+670
-49
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,23 @@ stainless-api-cli builds create \
3030
```
3131

3232
For details about specific commands, use the `--help` flag.
33+
34+
## Workspace Configuration
35+
36+
The CLI supports workspace configuration to avoid repeatedly specifying the project name. When you run a command, the CLI will:
37+
38+
1. Check if a project name is provided via command-line flag
39+
2. If not, look for a `stainless-workspace.json` file in the current directory or any parent directory
40+
3. Use the project name from the workspace configuration if found
41+
42+
### Initializing a Workspace
43+
44+
You can initialize a workspace configuration with:
45+
46+
```sh
47+
stainless-api-cli init --project-name your-project-name
48+
```
49+
50+
If you don't provide the `--project-name` flag, you'll be prompted to enter a project name.
51+
52+
Additionally, when you run a command with a project name flag in an interactive terminal, the CLI will offer to initialize a workspace configuration for you.

auth.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func handleAuthLogin(ctx context.Context, cmd *cli.Command) error {
5858
if err := SaveAuthConfig(config); err != nil {
5959
return fmt.Errorf("%s", au.Red(fmt.Sprintf("Failed to save authentication: %v", err)))
6060
}
61-
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authentication successful! Your credentials have been saved.")
61+
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authentication successful! Your credentials have been saved.")
6262
return nil
6363
}
6464

@@ -142,14 +142,14 @@ func handleAuthLogout(ctx context.Context, cmd *cli.Command) error {
142142
return fmt.Errorf("failed to remove auth file: %v", err)
143143
}
144144

145-
fmt.Printf("%s %s\n", au.BrightGreen(""), "Successfully logged out.")
145+
fmt.Printf("%s %s\n", au.BrightGreen(""), "Successfully logged out.")
146146
return nil
147147
}
148148

149149
func handleAuthStatus(ctx context.Context, cmd *cli.Command) error {
150150
// Check for API key in environment variables first
151151
if apiKey := os.Getenv("STAINLESS_API_KEY"); apiKey != "" {
152-
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authenticated via STAINLESS_API_KEY environment variable")
152+
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authenticated via STAINLESS_API_KEY environment variable")
153153
return nil
154154
}
155155

@@ -160,12 +160,12 @@ func handleAuthStatus(ctx context.Context, cmd *cli.Command) error {
160160
}
161161

162162
if config == nil {
163-
fmt.Printf("%s %s\n", au.BrightYellow("!"), "Not logged in.")
163+
fmt.Printf("%s %s\n", au.BrightYellow(""), "Not logged in.")
164164
return nil
165165
}
166166

167167
// If we have a config file with a token
168-
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authenticated via saved credentials")
168+
fmt.Printf("%s %s\n", au.BrightGreen(""), "Authenticated via saved credentials")
169169

170170
// Show a truncated version of the token for verification
171171
if len(config.AccessToken) > 10 {
@@ -318,5 +318,11 @@ func getClientOptions(ctx context.Context, cmd *cli.Command) []option.RequestOpt
318318
options = append(options, option.WithAPIKey(config.AccessToken))
319319
}
320320

321+
// Add default project from workspace config if available
322+
projectName := GetProjectNameFromConfig()
323+
if projectName != "" {
324+
options = append(options, option.WithProject(projectName))
325+
}
326+
321327
return options
322328
}

build.go

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package main
44

55
import (
6+
"bytes"
67
"context"
78
"fmt"
89
"io"
@@ -117,7 +118,8 @@ var buildsCreate = cli.Command{
117118
Action: getAPIFlagAction[bool]("body", "allow_empty"),
118119
},
119120
&cli.BoolFlag{
120-
Name: "wait",
121+
Name: "wait",
122+
Value: true,
121123
},
122124
&cli.BoolFlag{
123125
Name: "pull",
@@ -236,24 +238,23 @@ var buildsCompare = cli.Command{
236238

237239
func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
238240
cc := getAPICommandContext(ctx, cmd)
239-
// Log to stderr that we're creating a build (using white text)
240-
fmt.Fprintf(os.Stderr, "Creating build...\n")
241+
// Log to stderr that we're creating a build
242+
fmt.Fprintf(os.Stderr, "%s Creating build...\n", au.BrightCyan("✱"))
241243
params := stainlessv0.BuildNewParams{}
242244
res, err := cc.client.Builds.New(
243245
context.TODO(),
244246
params,
245247
option.WithMiddleware(cc.AsMiddleware()),
246-
option.WithRequestBody("application/json", cc.body),
247248
)
248249
if err != nil {
249250
return err
250251
}
251252

252-
// Print the build ID to stderr (using white text)
253-
fmt.Fprintf(os.Stderr, "Build created: %s\n", res.ID)
253+
// Print the build ID to stderr
254+
fmt.Fprintf(os.Stderr, " %s Build created: %s\n", au.BrightGreen("•"), au.Bold(res.ID))
254255

255256
if cmd.Bool("wait") {
256-
fmt.Fprintf(os.Stderr, "Waiting for build to complete...\n")
257+
fmt.Fprintf(os.Stderr, "%s Waiting for build to complete...\n", au.BrightCyan("✱"))
257258

258259
ticker := time.NewTicker(3 * time.Second)
259260
defer ticker.Stop()
@@ -289,14 +290,15 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
289290

290291
// Only print completed statuses with a green checkmark
291292
if isTargetCompleted(target.status) {
292-
fmt.Fprintf(os.Stderr, "%s Target %s: %s\n",
293-
au.BrightGreen(""),
293+
fmt.Fprintf(os.Stderr, " %s Target %s: %s\n",
294+
au.BrightGreen(""),
294295
target.name,
295296
string(target.status))
296297
anyCompleted = true
297298
} else if target.status == "failed" {
298299
// For failures, use red text
299-
fmt.Fprintf(os.Stderr, "Target %s: %s\n",
300+
fmt.Fprintf(os.Stderr, " %s Target %s: %s\n",
301+
au.BrightRed("•"),
300302
target.name,
301303
au.BrightRed(string(target.status)))
302304
}
@@ -310,7 +312,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
310312

311313
if (allCompleted || anyCompleted) && len(targets) > 0 {
312314
if allCompleted {
313-
fmt.Fprintf(os.Stderr, "%s Build completed successfully\n", au.BrightGreen(""))
315+
fmt.Fprintf(os.Stderr, " %s Build completed successfully\n", au.BrightGreen(""))
314316
break loop
315317
}
316318
}
@@ -321,11 +323,11 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
321323
}
322324

323325
if cmd.Bool("pull") {
324-
fmt.Fprintf(os.Stderr, "Pulling build outputs...\n")
326+
fmt.Fprintf(os.Stderr, "%s Pulling build outputs...\n", au.BrightCyan("✱"))
325327
if err := pullBuildOutputs(context.TODO(), cc.client, *res); err != nil {
326-
fmt.Fprintf(os.Stderr, "%s Failed to pull outputs: %v\n", au.BrightRed("!"), err)
328+
fmt.Fprintf(os.Stderr, "%s Failed to pull outputs: %v\n", au.BrightRed(""), err)
327329
} else {
328-
fmt.Fprintf(os.Stderr, "%s Successfully pulled all outputs\n", au.BrightGreen(""))
330+
fmt.Fprintf(os.Stderr, "%s Successfully pulled all outputs\n", au.BrightGreen(""))
329331
}
330332
}
331333
}
@@ -367,11 +369,12 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl
367369
return fmt.Errorf("no completed targets found in build %s", res.ID)
368370
}
369371

370-
fmt.Fprintf(os.Stderr, "Found completed targets: %v\n", targets)
371-
372372
// Pull each target
373-
for _, target := range targets {
374-
fmt.Fprintf(os.Stderr, "Pulling output for target: %s\n", target)
373+
for i, target := range targets {
374+
targetDir := fmt.Sprintf("%s-%s", res.Project, target)
375+
376+
fmt.Fprintf(os.Stderr, "%s [%d/%d] Pulling %s → %s\n",
377+
au.BrightCyan("✱"), i+1, len(targets), au.Bold(target), au.Cyan(targetDir))
375378

376379
// Get the output details
377380
outputRes, err := client.BuildTargetOutputs.Get(
@@ -384,57 +387,72 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl
384387
},
385388
)
386389
if err != nil {
387-
fmt.Fprintf(os.Stderr, "%s Error getting output for target %s: %v\n", au.BrightRed("!"), target, err)
390+
fmt.Fprintf(os.Stderr, "%s Failed to get output details for %s: %v\n",
391+
au.BrightRed("✱"), target, err)
388392
continue
389393
}
390394

391-
targetDir := fmt.Sprintf("%s-sdk", target)
392-
393395
// Handle based on output type
394-
err = pullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, targetDir)
396+
err = pullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, targetDir, target)
395397
if err != nil {
396-
fmt.Fprintf(os.Stderr, "%s Error pulling output for target %s: %v\n", au.BrightRed("!"), target, err)
398+
fmt.Fprintf(os.Stderr, "%s Failed to pull %s: %v\n",
399+
au.BrightRed("✱"), target, err)
397400
continue
398-
} else {
399-
fmt.Fprintf(os.Stderr, "%s Successfully pulled output for target %s\n", au.BrightGreen("✓"), target)
401+
}
402+
403+
fmt.Fprintf(os.Stderr, " %s Successfully pulled to %s\n",
404+
au.BrightBlack("•"), au.Cyan(targetDir))
405+
406+
if i < len(targets)-1 {
407+
fmt.Fprintf(os.Stderr, "\n")
400408
}
401409
}
402410

403411
return nil
404412
}
405413

406414
// pullOutput handles downloading or cloning a build target output
407-
func pullOutput(output, url, ref, targetDir string) error {
408-
// Create a directory for the output
415+
func pullOutput(output, url, ref, targetDir, target string) error {
416+
// Remove existing directory if it exists
417+
if _, err := os.Stat(targetDir); err == nil {
418+
fmt.Fprintf(os.Stderr, " %s Removing existing directory %s\n", au.BrightBlack("•"), targetDir)
419+
if err := os.RemoveAll(targetDir); err != nil {
420+
return fmt.Errorf("failed to remove existing directory %s: %v", targetDir, err)
421+
}
422+
}
423+
424+
// Create a fresh directory for the output
409425
if err := os.MkdirAll(targetDir, 0755); err != nil {
410426
return fmt.Errorf("failed to create directory %s: %v", targetDir, err)
411427
}
412428

413429
switch output {
414430
case "git":
415431
// Clone the repository
416-
fmt.Fprintf(os.Stderr, "Cloning repository %s (ref: %s) to %s\n", url, ref, targetDir)
432+
fmt.Fprintf(os.Stderr, " %s Cloning repository\n", au.BrightBlack("•"))
433+
fmt.Fprintf(os.Stderr, " %s Checking out ref %s\n", au.BrightBlack("•"), au.Bold(ref))
417434

418435
cmd := exec.Command("git", "clone", url, targetDir)
419-
cmd.Stdout = os.Stderr
420-
cmd.Stderr = os.Stderr
436+
var stderr bytes.Buffer
437+
cmd.Stdout = nil // Suppress git clone output
438+
cmd.Stderr = &stderr
421439
if err := cmd.Run(); err != nil {
422-
return fmt.Errorf("git clone failed: %v", err)
440+
return fmt.Errorf("git clone failed: %v\nGit error: %s", err, stderr.String())
423441
}
424442

425443
// Checkout the specific ref
426444
cmd = exec.Command("git", "-C", targetDir, "checkout", ref)
427-
cmd.Stdout = os.Stderr
428-
cmd.Stderr = os.Stderr
445+
stderr.Reset()
446+
cmd.Stdout = nil // Suppress git checkout output
447+
cmd.Stderr = &stderr
429448
if err := cmd.Run(); err != nil {
430-
return fmt.Errorf("git checkout failed: %v", err)
449+
return fmt.Errorf("git checkout failed: %v\nGit error: %s", err, stderr.String())
431450
}
432451

433-
fmt.Fprintf(os.Stderr, "%s Successfully cloned repository to %s\n", au.BrightGreen("✓"), targetDir)
434-
435452
case "url":
436453
// Download the tar file
437-
fmt.Fprintf(os.Stderr, "Downloading from %s to %s\n", url, targetDir)
454+
fmt.Fprintf(os.Stderr, " %s Downloading archive %s\n", au.BrightBlack("•"), au.Underline(url))
455+
fmt.Fprintf(os.Stderr, " %s Extracting to %s\n", au.BrightBlack("•"), targetDir)
438456

439457
// Create a temporary file for the tar download
440458
tmpFile, err := os.CreateTemp("", "stainless-*.tar.gz")
@@ -464,14 +482,12 @@ func pullOutput(output, url, ref, targetDir string) error {
464482

465483
// Extract the tar file
466484
cmd := exec.Command("tar", "-xzf", tmpFile.Name(), "-C", targetDir)
467-
cmd.Stdout = os.Stderr
468-
cmd.Stderr = os.Stderr
485+
cmd.Stdout = nil // Suppress tar output
486+
cmd.Stderr = nil
469487
if err := cmd.Run(); err != nil {
470488
return fmt.Errorf("tar extraction failed: %v", err)
471489
}
472490

473-
fmt.Fprintf(os.Stderr, "%s Successfully downloaded and extracted to %s\n", au.BrightGreen("✓"), targetDir)
474-
475491
default:
476492
return fmt.Errorf("unsupported output type: %s. Supported types are 'git' and 'url'", output)
477493
}
@@ -502,7 +518,6 @@ func handleBuildsCompare(ctx context.Context, cmd *cli.Command) error {
502518
context.TODO(),
503519
params,
504520
option.WithMiddleware(cc.AsMiddleware()),
505-
option.WithRequestBody("application/json", cc.body),
506521
)
507522
if err != nil {
508523
return err

buildtargetoutput.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,5 @@ func handleBuildTargetOutputsPull(ctx context.Context, cmd *cli.Command) error {
9595
targetDir := fmt.Sprintf("%s-%s", "tmp", "target")
9696

9797
// Use the shared pullOutput function
98-
return pullOutput(res.Output, res.URL, res.Ref, targetDir)
98+
return pullOutput(res.Output, res.URL, res.Ref, targetDir, "target")
9999
}

go.mod

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ go 1.23.0
55
toolchain go1.23.9
66

77
require (
8-
github.com/stainless-api/stainless-api-go v0.5.1
8+
github.com/charmbracelet/bubbles v0.21.0
9+
github.com/charmbracelet/huh v0.7.0
10+
github.com/charmbracelet/lipgloss v1.1.0
911
github.com/logrusorgru/aurora/v4 v4.0.0
1012
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
13+
github.com/stainless-api/stainless-api-go v0.5.1
1114
github.com/tidwall/gjson v1.17.0
1215
github.com/tidwall/pretty v1.2.1
1316
github.com/tidwall/sjson v1.2.5
@@ -16,6 +19,29 @@ require (
1619
)
1720

1821
require (
22+
github.com/atotto/clipboard v0.1.4 // indirect
23+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
24+
github.com/catppuccin/go v0.3.0 // indirect
25+
github.com/charmbracelet/bubbletea v1.3.4 // indirect
26+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
27+
github.com/charmbracelet/x/ansi v0.8.0 // indirect
28+
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
29+
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
30+
github.com/charmbracelet/x/term v0.2.1 // indirect
31+
github.com/dustin/go-humanize v1.0.1 // indirect
32+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
33+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
34+
github.com/mattn/go-isatty v0.0.20 // indirect
35+
github.com/mattn/go-localereader v0.0.1 // indirect
36+
github.com/mattn/go-runewidth v0.0.16 // indirect
37+
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
38+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
39+
github.com/muesli/cancelreader v0.2.2 // indirect
40+
github.com/muesli/termenv v0.16.0 // indirect
41+
github.com/rivo/uniseg v0.4.7 // indirect
1942
github.com/tidwall/match v1.1.1 // indirect
43+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
44+
golang.org/x/sync v0.12.0 // indirect
2045
golang.org/x/sys v0.33.0 // indirect
46+
golang.org/x/text v0.23.0 // indirect
2147
)

0 commit comments

Comments
 (0)