Skip to content

Commit d5062fb

Browse files
committed
feat: convert paths to absolute path in the .stainless/workspace.json
1 parent 38f9d33 commit d5062fb

File tree

4 files changed

+140
-44
lines changed

4 files changed

+140
-44
lines changed

pkg/cmd/init.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@ func askSelectProject(projects []stainless.Project) (string, []stainless.Target,
269269
}
270270

271271
func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandContext, org, projectName string) (string, []stainless.Target, error) {
272-
info := console.Info("project (new)")
272+
group := console.Property("project", "(new)")
273273

274274
if projectName == "" {
275-
err := info.Field(huh.NewInput().
275+
err := group.Field(huh.NewInput().
276276
Title("name").
277277
Description("Enter a display name for your new project").
278278
Value(&projectName).
@@ -292,7 +292,7 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
292292
return "", nil, err
293293
}
294294
}
295-
info.Property("name", projectName)
295+
group.Property("name", projectName)
296296

297297
// Determine targets
298298
var selectedTargets []stainless.Target
@@ -312,7 +312,7 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
312312
for i, target := range allTargets {
313313
options[i] = huh.NewOption(target.DisplayName, stainless.Target(target.Name)).Selected(target.DefaultSelected)
314314
}
315-
err := info.Field(huh.NewMultiSelect[stainless.Target]().
315+
err := group.Field(huh.NewMultiSelect[stainless.Target]().
316316
Title("targets").
317317
Description("Select target languages for code generation").
318318
Options(options...).
@@ -328,7 +328,7 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
328328
}
329329
}
330330

331-
info.Property("targets", fmt.Sprintf("%v", selectedTargets))
331+
group.Property("targets", fmt.Sprintf("%v", selectedTargets))
332332

333333
slug := nameToSlug(projectName)
334334
params := stainless.ProjectNewParams{
@@ -340,7 +340,7 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
340340
}
341341

342342
// Get OpenAPI spec content
343-
oasContent, err := askExistingOpenAPISpec(info)
343+
oasContent, err := askExistingOpenAPISpec(group)
344344
if err != nil {
345345
return "", nil, err
346346
}
@@ -354,8 +354,6 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
354354
oasName = "openapi.yml"
355355
}
356356

357-
info.Property("openapi", "petstore.yml")
358-
359357
params.Revision[oasName] = stainless.FileInputUnionParam{
360358
OfFileInputContent: &stainless.FileInputContentParam{
361359
Content: oasContent,
@@ -371,7 +369,7 @@ func askCreateProject(ctx context.Context, cmd *cli.Command, cc *apiCommandConte
371369
return "", nil, err
372370
}
373371

374-
info.Success("Project created successfully")
372+
group.Success("Project created successfully")
375373
return slug, selectedTargets, nil
376374
}
377375

@@ -505,10 +503,12 @@ func askExistingOpenAPISpec(group console.Group) (content string, err error) {
505503
// Use file picker to select file
506504
var filePath string
507505
err = group.Field(huh.NewFilePicker().
506+
Picking(true).
507+
CurrentDirectory(".").
508508
Title("openapi_spec (file)").
509509
Description("Select your OpenAPI spec file").
510-
CurrentDirectory(".").
511-
AllowedTypes([]string{".json", ".yaml", ".yml"}).
510+
ShowHidden(true).
511+
Height(10).
512512
Value(&filePath))
513513
if err != nil {
514514
return "", err
@@ -524,6 +524,8 @@ func askExistingOpenAPISpec(group console.Group) (content string, err error) {
524524
return "", fmt.Errorf("failed to read OpenAPI spec from %s: %w", filePath, err)
525525
}
526526

527+
group.Property("openapi", filePath)
528+
527529
return string(fileBytes), nil
528530

529531
case SourceURL:
@@ -559,9 +561,13 @@ func askExistingOpenAPISpec(group console.Group) (content string, err error) {
559561
return "", fmt.Errorf("failed to read response body: %w", err)
560562
}
561563

564+
group.Property("openapi", urlStr)
565+
562566
return string(bodyBytes), nil
563567

564568
case SourceExample:
569+
group.Property("openapi", "petstore.yml")
570+
565571
return exampleSpecJSON, nil
566572

567573
default:
@@ -726,11 +732,15 @@ func configureTargets(slug string, targets []stainless.Target, config *Workspace
726732

727733
group := console.Info("Configuring targets...")
728734

729-
// Initialize target configs with default paths
735+
// Initialize target configs with default absolute paths
730736
targetConfigs := make(map[stainless.Target]*TargetConfig, len(targets))
737+
cwd, err := os.Getwd()
738+
if err != nil {
739+
return fmt.Errorf("failed to get current directory: %w", err)
740+
}
731741
for _, target := range targets {
732742
defaultPath := filepath.Join("sdks", fmt.Sprintf("%s-%s", slug, target))
733-
targetConfigs[target] = &TargetConfig{OutputPath: defaultPath}
743+
targetConfigs[target] = &TargetConfig{OutputPath: Resolve(cwd, defaultPath)}
734744
}
735745

736746
// Create form fields for each target
@@ -753,10 +763,14 @@ func configureTargets(slug string, targets []stainless.Target, config *Workspace
753763
return fmt.Errorf("failed to get target output paths: %v", err)
754764
}
755765

756-
// Update config with user-provided paths
766+
// Update config with user-provided paths (convert to absolute)
757767
for target, pathVar := range pathVars {
758768
if path := strings.TrimSpace(*pathVar); path != "" {
759-
targetConfigs[target] = &TargetConfig{OutputPath: path}
769+
absPath, err := filepath.Abs(path)
770+
if err != nil {
771+
return fmt.Errorf("failed to get absolute path for %s target: %w", target, err)
772+
}
773+
targetConfigs[target] = &TargetConfig{OutputPath: absPath}
760774
} else {
761775
delete(targetConfigs, target)
762776
}

pkg/cmd/util.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/http/httputil"
1212
"net/url"
1313
"os"
14-
"path/filepath"
1514
"slices"
1615
"strings"
1716

@@ -189,19 +188,15 @@ func getAPICommandContext(cmd *cli.Command) *apiCommandContext {
189188
}
190189

191190
config := workspaceConfig
192-
// Get the directory containing the workspace config file
193-
configDir := filepath.Dir(config.ConfigPath)
194191

195192
if slices.Contains(names, "openapi-spec") && !cmd.IsSet("openapi-spec") && !cmd.IsSet("revision") && config.OpenAPISpec != "" {
196-
// Set OpenAPI spec path relative to workspace config directory
197-
openAPIPath := filepath.Join(configDir, config.OpenAPISpec)
198-
cmd.Set("openapi-spec", openAPIPath)
193+
// OpenAPI spec path is already absolute
194+
cmd.Set("openapi-spec", config.OpenAPISpec)
199195
}
200196

201197
if slices.Contains(names, "stainless-config") && !cmd.IsSet("stainless-config") && !cmd.IsSet("revision") && config.StainlessConfig != "" {
202-
// Set Stainless config path relative to workspace config directory
203-
stainlessConfigPath := filepath.Join(configDir, config.StainlessConfig)
204-
cmd.Set("stainless-config", stainlessConfigPath)
198+
// Stainless config path is already absolute
199+
cmd.Set("stainless-config", config.StainlessConfig)
205200
}
206201

207202
if slices.Contains(names, "project") && !cmd.IsSet("project") && config.Project != "" {

pkg/cmd/workspace.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,8 @@ func handleWorkspaceStatus(ctx context.Context, cmd *cli.Command) error {
101101
group.Property("project", cc.workspaceConfig.Project)
102102

103103
if cc.workspaceConfig.OpenAPISpec != "" {
104-
// Check if OpenAPI spec file exists
105-
configDir := filepath.Dir(cc.workspaceConfig.ConfigPath)
106-
specPath := filepath.Join(configDir, cc.workspaceConfig.OpenAPISpec)
107-
if _, err := os.Stat(specPath); err == nil {
104+
// Check if OpenAPI spec file exists (path is already absolute)
105+
if _, err := os.Stat(cc.workspaceConfig.OpenAPISpec); err == nil {
108106
group.Property("openapi_spec", cc.workspaceConfig.OpenAPISpec)
109107
} else {
110108
group.Property("openapi_spec", cc.workspaceConfig.OpenAPISpec+" (not found)")
@@ -114,10 +112,8 @@ func handleWorkspaceStatus(ctx context.Context, cmd *cli.Command) error {
114112
}
115113

116114
if cc.workspaceConfig.StainlessConfig != "" {
117-
// Check if Stainless config file exists
118-
configDir := filepath.Dir(cc.workspaceConfig.ConfigPath)
119-
stainlessPath := filepath.Join(configDir, cc.workspaceConfig.StainlessConfig)
120-
if _, err := os.Stat(stainlessPath); err == nil {
115+
// Check if Stainless config file exists (path is already absolute)
116+
if _, err := os.Stat(cc.workspaceConfig.StainlessConfig); err == nil {
121117
group.Property("stainless_config", cc.workspaceConfig.StainlessConfig)
122118
} else {
123119
group.Property("stainless_config", cc.workspaceConfig.StainlessConfig+" (not found)")

pkg/cmd/workspaceconfig.go

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,35 @@ import (
1010
"github.com/stainless-api/stainless-api-go"
1111
)
1212

13+
// Resolve converts a path to absolute, resolving it relative to baseDir if it's not already absolute
14+
func Resolve(baseDir, path string) string {
15+
if filepath.IsAbs(path) {
16+
return filepath.Clean(path)
17+
}
18+
return filepath.Clean(filepath.Join(baseDir, path))
19+
}
20+
1321
// TargetConfig stores configuration for a specific SDK target
1422
type TargetConfig struct {
1523
OutputPath string `json:"output_path"`
1624
}
1725

18-
// WorkspaceConfig stores workspace-level configuration
19-
type WorkspaceConfig struct {
20-
Project string `json:"project"`
21-
OpenAPISpec string `json:"openapi_spec,omitempty"`
22-
StainlessConfig string `json:"stainless_config,omitempty"`
26+
// WorkspaceConfigExport represents the on-disk format with relative paths
27+
type WorkspaceConfigExport struct {
28+
Project string `json:"project"`
29+
OpenAPISpec string `json:"openapi_spec,omitempty"`
30+
StainlessConfig string `json:"stainless_config,omitempty"`
2331
Targets map[stainless.Target]*TargetConfig `json:"targets,omitempty"`
32+
}
2433

25-
ConfigPath string `json:"-"`
34+
// WorkspaceConfig stores workspace-level configuration with absolute paths
35+
type WorkspaceConfig struct {
36+
Project string
37+
OpenAPISpec string
38+
StainlessConfig string
39+
Targets map[stainless.Target]*TargetConfig
40+
41+
ConfigPath string
2642
}
2743

2844
// Find searches for a stainless-workspace.json file starting from the current directory
@@ -82,20 +98,84 @@ func (config *WorkspaceConfig) Load(configPath string) error {
8298
return nil
8399
}
84100

85-
if err := json.NewDecoder(file).Decode(config); err != nil {
101+
// Load into export format (with relative paths)
102+
var export WorkspaceConfigExport
103+
if err := json.NewDecoder(file).Decode(&export); err != nil {
86104
return fmt.Errorf("failed to parse workspace config file %s: %w", configPath, err)
87105
}
106+
107+
// Get the directory containing the config file
108+
configDir := filepath.Dir(configPath)
109+
110+
// Convert relative paths to absolute paths
111+
config.Project = export.Project
88112
config.ConfigPath = configPath
113+
114+
if export.OpenAPISpec != "" {
115+
config.OpenAPISpec = Resolve(configDir, export.OpenAPISpec)
116+
}
117+
118+
if export.StainlessConfig != "" {
119+
config.StainlessConfig = Resolve(configDir, export.StainlessConfig)
120+
}
121+
122+
// Convert target paths to absolute
123+
if export.Targets != nil {
124+
config.Targets = make(map[stainless.Target]*TargetConfig, len(export.Targets))
125+
for target, targetConfig := range export.Targets {
126+
config.Targets[target] = &TargetConfig{
127+
OutputPath: Resolve(configDir, targetConfig.OutputPath),
128+
}
129+
}
130+
}
131+
89132
return nil
90133
}
91134

92135
func (config *WorkspaceConfig) Save() error {
93136
// Create parent directories if they don't exist
94-
dir := filepath.Dir(config.ConfigPath)
95-
if err := os.MkdirAll(dir, 0755); err != nil {
137+
configDir := filepath.Dir(config.ConfigPath)
138+
if err := os.MkdirAll(configDir, 0755); err != nil {
96139
return fmt.Errorf("failed to create directory for config file: %w", err)
97140
}
98141

142+
// Convert absolute paths to relative paths for export
143+
export := WorkspaceConfigExport{
144+
Project: config.Project,
145+
}
146+
147+
// Convert paths to relative (fallback to absolute if conversion fails)
148+
if config.OpenAPISpec != "" {
149+
if relPath, err := filepath.Rel(configDir, config.OpenAPISpec); err == nil {
150+
export.OpenAPISpec = relPath
151+
} else {
152+
println(err.Error())
153+
export.OpenAPISpec = config.OpenAPISpec
154+
}
155+
}
156+
157+
if config.StainlessConfig != "" {
158+
if relPath, err := filepath.Rel(configDir, config.StainlessConfig); err == nil {
159+
export.StainlessConfig = relPath
160+
} else {
161+
println(err.Error())
162+
export.StainlessConfig = config.StainlessConfig
163+
}
164+
}
165+
166+
if config.Targets != nil {
167+
export.Targets = make(map[stainless.Target]*TargetConfig, len(config.Targets))
168+
for target, targetConfig := range config.Targets {
169+
outputPath := targetConfig.OutputPath
170+
if relPath, err := filepath.Rel(configDir, outputPath); err == nil {
171+
outputPath = relPath
172+
}
173+
export.Targets[target] = &TargetConfig{
174+
OutputPath: outputPath,
175+
}
176+
}
177+
}
178+
99179
file, err := os.Create(config.ConfigPath)
100180
if err != nil {
101181
return err
@@ -104,7 +184,7 @@ func (config *WorkspaceConfig) Save() error {
104184

105185
encoder := json.NewEncoder(file)
106186
encoder.SetIndent("", " ")
107-
return encoder.Encode(config)
187+
return encoder.Encode(export)
108188
}
109189

110190
func NewWorkspaceConfig(projectName, openAPISpecPath, stainlessConfigPath string) (WorkspaceConfig, error) {
@@ -113,10 +193,21 @@ func NewWorkspaceConfig(projectName, openAPISpecPath, stainlessConfigPath string
113193
return WorkspaceConfig{}, err
114194
}
115195

196+
// Convert paths to absolute
197+
absOpenAPISpec, err := filepath.Abs(openAPISpecPath)
198+
if err != nil {
199+
return WorkspaceConfig{}, fmt.Errorf("failed to get absolute path for OpenAPI spec: %w", err)
200+
}
201+
202+
absStainlessConfig, err := filepath.Abs(stainlessConfigPath)
203+
if err != nil {
204+
return WorkspaceConfig{}, fmt.Errorf("failed to get absolute path for Stainless config: %w", err)
205+
}
206+
116207
return WorkspaceConfig{
117208
Project: projectName,
118-
OpenAPISpec: openAPISpecPath,
119-
StainlessConfig: stainlessConfigPath,
209+
OpenAPISpec: absOpenAPISpec,
210+
StainlessConfig: absStainlessConfig,
120211
Targets: nil,
121212
ConfigPath: filepath.Join(dir, ".stainless", "workspace.json"),
122213
}, nil

0 commit comments

Comments
 (0)