@@ -6,7 +6,10 @@ import (
66 "context"
77 "fmt"
88 "os"
9+ "strings"
910
11+ "github.com/charmbracelet/huh"
12+ "github.com/logrusorgru/aurora/v4"
1013 "github.com/stainless-api/stainless-api-cli/pkg/jsonflag"
1114 "github.com/stainless-api/stainless-api-go"
1215 "github.com/stainless-api/stainless-api-go/option"
@@ -52,6 +55,21 @@ var projectsCreate = cli.Command{
5255 Path : "targets.-1" ,
5356 },
5457 },
58+ & cli.StringFlag {
59+ Name : "openapi-spec" ,
60+ Aliases : []string {"oas" },
61+ Usage : "Path to OpenAPI spec file" ,
62+ },
63+ & cli.StringFlag {
64+ Name : "stainless-config" ,
65+ Aliases : []string {"config" },
66+ Usage : "Path to Stainless config file" ,
67+ },
68+ & cli.BoolFlag {
69+ Name : "init-workspace" ,
70+ Usage : "Initialize workspace configuration after creating project" ,
71+ Value : true , // Default to true
72+ },
5573 },
5674 Action : handleProjectsCreate ,
5775 HideHelpCommand : true ,
@@ -119,6 +137,190 @@ var projectsList = cli.Command{
119137}
120138
121139func handleProjectsCreate (ctx context.Context , cmd * cli.Command ) error {
140+ // Define available target languages
141+ availableTargets := []huh.Option [string ]{
142+ huh .NewOption ("TypeScript" , "typescript" ).Selected (true ),
143+ huh .NewOption ("Python" , "python" ).Selected (true ),
144+ huh .NewOption ("Go" , "go" ),
145+ huh .NewOption ("Java" , "java" ),
146+ huh .NewOption ("Kotlin" , "kotlin" ),
147+ huh .NewOption ("Ruby" , "ruby" ),
148+ huh .NewOption ("Terraform" , "terraform" ),
149+ huh .NewOption ("C#" , "csharp" ),
150+ huh .NewOption ("PHP" , "php" ),
151+ }
152+
153+ // Get values from flags
154+ org := cmd .String ("org" )
155+ projectName := cmd .String ("display-name" ) // Keep display-name flag for compatibility
156+ if projectName == "" {
157+ projectName = cmd .String ("slug" ) // Also check slug flag for compatibility
158+ }
159+ targetsFlag := cmd .String ("targets" )
160+ openAPISpec := cmd .String ("openapi-spec" )
161+ stainlessConfig := cmd .String ("stainless-config" )
162+ initWorkspace := cmd .Bool ("init-workspace" )
163+
164+ // Convert comma-separated targets flag to slice for multi-select
165+ var selectedTargets []string
166+ if targetsFlag != "" {
167+ for _ , target := range strings .Split (targetsFlag , "," ) {
168+ selectedTargets = append (selectedTargets , strings .TrimSpace (target ))
169+ }
170+ }
171+
172+ // Pre-fill OpenAPI spec and Stainless config if found and not provided via flags
173+ if openAPISpec == "" {
174+ openAPISpec = findOpenAPISpec ()
175+ }
176+ if stainlessConfig == "" {
177+ stainlessConfig = findStainlessConfig ()
178+ }
179+
180+ // Check if all required values are provided via flags
181+ // OpenAPI spec and Stainless config are optional
182+ allValuesProvided := org != "" && projectName != ""
183+
184+ if ! allValuesProvided {
185+ fmt .Println ("Creating a new project..." )
186+ fmt .Println ()
187+
188+ // Fetch available organizations for suggestions
189+ orgs := fetchUserOrgs (ctx )
190+
191+ // Auto-fill with first organization if org is empty and orgs are available
192+ if org == "" && len (orgs ) > 0 {
193+ org = orgs [0 ]
194+ }
195+
196+ form := huh .NewForm (
197+ huh .NewGroup (
198+ huh .NewInput ().
199+ Title ("organization" ).
200+ Value (& org ).
201+ Suggestions (orgs ).
202+ Description ("Enter the organization for this project" ).
203+ Validate (func (s string ) error {
204+ if strings .TrimSpace (s ) == "" {
205+ return fmt .Errorf ("organization is required" )
206+ }
207+ return nil
208+ }),
209+ huh .NewInput ().
210+ Title ("project name" ).
211+ Value (& projectName ).
212+ DescriptionFunc (func () string {
213+ if projectName == "" {
214+ return "Project name, slug will be 'my-project'."
215+ }
216+ slug := nameToSlug (projectName )
217+ return fmt .Sprintf ("Project name, slug will be '%s'." , slug )
218+ }, & projectName ).
219+ Placeholder ("My Project" ).
220+ Validate (func (s string ) error {
221+ if strings .TrimSpace (s ) == "" {
222+ return fmt .Errorf ("project name is required" )
223+ }
224+ return nil
225+ }),
226+ ).Title ("Page (1/2)" ),
227+ huh .NewGroup (
228+ huh .NewMultiSelect [string ]().
229+ Title ("targets" ).
230+ Description ("Select target languages for code generation" ).
231+ Options (availableTargets ... ).
232+ Value (& selectedTargets ),
233+ huh .NewInput ().
234+ Title ("OpenAPI spec path" ).
235+ Description ("Relative path to your OpenAPI spec file" ).
236+ Placeholder ("openapi.yml" ).
237+ Validate (func (s string ) error {
238+ s = strings .TrimSpace (s )
239+ if s == "" {
240+ return fmt .Errorf ("openapi spec is required" )
241+ }
242+ if _ , err := os .Stat (s ); os .IsNotExist (err ) {
243+ return fmt .Errorf ("file '%s' does not exist" , s )
244+ }
245+ return nil
246+ }).
247+ Value (& openAPISpec ),
248+ huh .NewInput ().
249+ Title ("Stainless config path (optional)" ).
250+ Description ("Relative path to your Stainless config file" ).
251+ Placeholder ("openapi.stainless.yml" ).
252+ Validate (func (s string ) error {
253+ s = strings .TrimSpace (s )
254+ if s == "" {
255+ return nil
256+ }
257+ if _ , err := os .Stat (s ); os .IsNotExist (err ) {
258+ return fmt .Errorf ("file '%s' does not exist" , s )
259+ }
260+ return nil
261+ }).
262+ Value (& stainlessConfig ),
263+ ).Title ("Page (2/2)" ),
264+ ).WithTheme (GetFormTheme ()).WithKeyMap (GetFormKeyMap ())
265+
266+ if err := form .Run (); err != nil {
267+ return fmt .Errorf ("failed to get project configuration: %v" , err )
268+ }
269+
270+ // Generate slug from project name
271+ slug := nameToSlug (projectName )
272+
273+ fmt .Printf ("%s organization: %s\n " , aurora .Bold ("✱" ), org )
274+ fmt .Printf ("%s project name: %s\n " , aurora .Bold ("✱" ), projectName )
275+ fmt .Printf ("%s slug: %s\n " , aurora .Bold ("✱" ), slug )
276+ if len (selectedTargets ) > 0 {
277+ fmt .Printf ("%s targets: %s\n " , aurora .Bold ("✱" ), strings .Join (selectedTargets , ", " ))
278+ }
279+ if openAPISpec != "" {
280+ fmt .Printf ("%s openapi spec: %s\n " , aurora .Bold ("✱" ), openAPISpec )
281+ }
282+ if stainlessConfig != "" {
283+ fmt .Printf ("%s stainless config: %s\n " , aurora .Bold ("✱" ), stainlessConfig )
284+ }
285+ fmt .Println ()
286+
287+ // Set the flag values so the JSONFlag middleware can pick them up
288+ cmd .Set ("org" , org )
289+ cmd .Set ("display-name" , projectName )
290+ cmd .Set ("slug" , slug )
291+ if len (selectedTargets ) > 0 {
292+ cmd .Set ("targets" , strings .Join (selectedTargets , "," ))
293+ }
294+ if openAPISpec != "" {
295+ cmd .Set ("openapi-spec" , openAPISpec )
296+ }
297+ if stainlessConfig != "" {
298+ cmd .Set ("stainless-config" , stainlessConfig )
299+ }
300+ } else {
301+ // Generate slug from project name for non-interactive mode too
302+ slug := nameToSlug (projectName )
303+ cmd .Set ("slug" , slug )
304+ }
305+
306+ // Inject file contents into the API payload if files are provided or found
307+ if openAPISpec != "" {
308+ content , err := os .ReadFile (openAPISpec )
309+ if err == nil {
310+ // Inject the actual file content into the project creation payload
311+ jsonflag .Register (jsonflag .Body , "revision.openapi\\ .yml.content" , string (content ))
312+ }
313+ }
314+
315+ if stainlessConfig != "" {
316+ content , err := os .ReadFile (stainlessConfig )
317+ if err == nil {
318+ // Inject the actual file content into the project creation payload
319+ jsonflag .Register (jsonflag .Body , "revision.openapi\\ .stainless\\ .yml.content" , string (content ))
320+ }
321+ }
322+
323+ // Use the original logic - let the JSONFlag middleware handle parameter construction
122324 cc := getAPICommandContext (cmd )
123325 params := stainlessv0.ProjectNewParams {}
124326 res , err := cc .client .Projects .New (
@@ -130,7 +332,27 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error {
130332 return err
131333 }
132334
335+ if ! allValuesProvided {
336+ fmt .Printf ("%s %s\n " , aurora .BrightGreen ("✱" ), fmt .Sprintf ("Project created successfully" ))
337+ }
133338 fmt .Printf ("%s\n " , ColorizeJSON (res .RawJSON (), os .Stdout ))
339+
340+ // Initialize workspace if requested
341+ if initWorkspace {
342+ fmt .Println ()
343+ fmt .Printf ("Initializing workspace configuration...\n " )
344+
345+ // Use the same project name (slug) for workspace initialization
346+ slug := nameToSlug (projectName )
347+ err := InitWorkspaceConfig (slug , openAPISpec , stainlessConfig )
348+ if err != nil {
349+ fmt .Printf ("%s Failed to initialize workspace: %v\n " , aurora .BrightRed ("✱" ), err )
350+ return fmt .Errorf ("project created but workspace initialization failed: %v" , err )
351+ }
352+
353+ fmt .Printf ("%s %s\n " , aurora .BrightGreen ("✱" ), fmt .Sprintf ("Workspace initialized" ))
354+ }
355+
134356 return nil
135357}
136358
@@ -187,3 +409,55 @@ func handleProjectsList(ctx context.Context, cmd *cli.Command) error {
187409 fmt .Printf ("%s\n " , ColorizeJSON (res .RawJSON (), os .Stdout ))
188410 return nil
189411}
412+
413+ // fetchUserOrgs retrieves the list of organizations the user has access to
414+ func fetchUserOrgs (ctx context.Context ) []string {
415+ client := stainlessv0 .NewClient (getClientOptions ()... )
416+
417+ res , err := client .Orgs .List (ctx )
418+ if err != nil {
419+ // Return empty slice if we can't fetch orgs
420+ return []string {}
421+ }
422+
423+ var orgs []string
424+ for _ , org := range res .Data {
425+ if org .Slug != "" {
426+ orgs = append (orgs , org .Slug )
427+ }
428+ }
429+
430+ return orgs
431+ }
432+
433+ // nameToSlug converts a project name to a URL-friendly slug
434+ func nameToSlug (name string ) string {
435+ // Convert to lowercase
436+ slug := strings .ToLower (name )
437+
438+ // Replace spaces and common punctuation with hyphens
439+ slug = strings .ReplaceAll (slug , " " , "-" )
440+ slug = strings .ReplaceAll (slug , "_" , "-" )
441+ slug = strings .ReplaceAll (slug , "." , "-" )
442+ slug = strings .ReplaceAll (slug , "/" , "-" )
443+ slug = strings .ReplaceAll (slug , "\\ " , "-" )
444+
445+ // Remove any characters that aren't alphanumeric or hyphens
446+ var result strings.Builder
447+ for _ , r := range slug {
448+ if (r >= 'a' && r <= 'z' ) || (r >= '0' && r <= '9' ) || r == '-' {
449+ result .WriteRune (r )
450+ }
451+ }
452+ slug = result .String ()
453+
454+ // Remove multiple consecutive hyphens
455+ for strings .Contains (slug , "--" ) {
456+ slug = strings .ReplaceAll (slug , "--" , "-" )
457+ }
458+
459+ // Trim hyphens from start and end
460+ slug = strings .Trim (slug , "-" )
461+
462+ return slug
463+ }
0 commit comments