@@ -77,14 +77,18 @@ var configureGenerationCmd = &model.CommandGroup{
7777}
7878
7979type ConfigureSourcesFlags struct {
80- ID string `json:"id"`
81- New bool `json:"new"`
80+ ID string `json:"id"`
81+ New bool `json:"new"`
82+ Location string `json:"location"`
83+ SourceName string `json:"source-name"`
84+ AuthHeader string `json:"auth-header"`
85+ OutputPath string `json:"output"`
8286}
8387
8488var configureSourcesCmd = & model.ExecutableCommand [ConfigureSourcesFlags ]{
8589 Usage : "sources" ,
8690 Short : "Configure new or existing sources." ,
87- Long : "Guided prompts to configure a new or existing source in your speakeasy workflow." ,
91+ Long : "Guided prompts to configure a new or existing source in your speakeasy workflow. When --location and --source-name are provided, runs in non-interactive mode suitable for CI/CD. " ,
8892 Run : configureSources ,
8993 RequiresAuth : true ,
9094 Flags : []flag.Flag {
@@ -98,18 +102,44 @@ var configureSourcesCmd = &model.ExecutableCommand[ConfigureSourcesFlags]{
98102 Shorthand : "n" ,
99103 Description : "configure a new source" ,
100104 },
105+ flag.StringFlag {
106+ Name : "location" ,
107+ Shorthand : "l" ,
108+ Description : "location of the OpenAPI document (local file path or URL); enables non-interactive mode when combined with --source-name" ,
109+ },
110+ flag.StringFlag {
111+ Name : "source-name" ,
112+ Shorthand : "s" ,
113+ Description : "name for the source; enables non-interactive mode when combined with --location" ,
114+ },
115+ flag.StringFlag {
116+ Name : "auth-header" ,
117+ Description : "authentication header name for remote documents (value from $OPENAPI_DOC_AUTH_TOKEN)" ,
118+ },
119+ flag.StringFlag {
120+ Name : "output" ,
121+ Shorthand : "o" ,
122+ Description : "output path for the compiled source document" ,
123+ },
101124 },
102125}
103126
104127type ConfigureTargetFlags struct {
105- ID string `json:"id"`
106- New bool `json:"new"`
128+ ID string `json:"id"`
129+ New bool `json:"new"`
130+ TargetType string `json:"target-type"`
131+ SourceID string `json:"source"`
132+ TargetName string `json:"target-name"`
133+ SDKClassName string `json:"sdk-class-name"`
134+ PackageName string `json:"package-name"`
135+ BaseServerURL string `json:"base-server-url"`
136+ OutputDir string `json:"output"`
107137}
108138
109139var configureTargetCmd = & model.ExecutableCommand [ConfigureTargetFlags ]{
110140 Usage : "targets" ,
111141 Short : "Configure new or existing targets." ,
112- Long : "Guided prompts to configure a new or existing target in your speakeasy workflow." ,
142+ Long : "Guided prompts to configure a new or existing target in your speakeasy workflow. When --target-type and --source are provided, runs in non-interactive mode suitable for CI/CD. " ,
113143 Run : configureTarget ,
114144 RequiresAuth : true ,
115145 Flags : []flag.Flag {
@@ -123,6 +153,37 @@ var configureTargetCmd = &model.ExecutableCommand[ConfigureTargetFlags]{
123153 Shorthand : "n" ,
124154 Description : "configure a new target" ,
125155 },
156+ flag.StringFlag {
157+ Name : "target-type" ,
158+ Shorthand : "t" ,
159+ Description : "target language/type: typescript, python, go, java, csharp, php, ruby, terraform, mcp-typescript; enables non-interactive mode" ,
160+ },
161+ flag.StringFlag {
162+ Name : "source" ,
163+ Shorthand : "s" ,
164+ Description : "name of the source to generate from; enables non-interactive mode when combined with --target-type" ,
165+ },
166+ flag.StringFlag {
167+ Name : "target-name" ,
168+ Description : "name for the target (defaults to target-type if not specified)" ,
169+ },
170+ flag.StringFlag {
171+ Name : "sdk-class-name" ,
172+ Description : "SDK class name (PascalCase, e.g., MyCompanySDK)" ,
173+ },
174+ flag.StringFlag {
175+ Name : "package-name" ,
176+ Description : "package name for the generated SDK" ,
177+ },
178+ flag.StringFlag {
179+ Name : "base-server-url" ,
180+ Description : "base server URL for the SDK" ,
181+ },
182+ flag.StringFlag {
183+ Name : "output" ,
184+ Shorthand : "o" ,
185+ Description : "output directory for the generated target" ,
186+ },
126187 },
127188}
128189
@@ -218,6 +279,11 @@ func configureSources(ctx context.Context, flags ConfigureSourcesFlags) error {
218279 }
219280 }
220281
282+ // Non-interactive mode: when both --location and --source-name are provided
283+ if flags .Location != "" && flags .SourceName != "" {
284+ return configureSourcesNonInteractive (ctx , workingDir , workflowFile , flags )
285+ }
286+
221287 var existingSourceName string
222288 var existingSource * workflow.Source
223289 if source , ok := workflowFile .Sources [flags .ID ]; ok {
@@ -291,6 +357,81 @@ func configureSources(ctx context.Context, flags ConfigureSourcesFlags) error {
291357 return nil
292358}
293359
360+ // configureSourcesNonInteractive handles source configuration without interactive prompts.
361+ // This is used when --location and --source-name flags are both provided.
362+ func configureSourcesNonInteractive (ctx context.Context , workingDir string , workflowFile * workflow.Workflow , flags ConfigureSourcesFlags ) error {
363+ logger := log .From (ctx )
364+
365+ // Validate source name doesn't already exist
366+ if _ , ok := workflowFile .Sources [flags .SourceName ]; ok {
367+ return fmt .Errorf ("a source with the name %q already exists" , flags .SourceName )
368+ }
369+
370+ // Validate source name format
371+ if strings .Contains (flags .SourceName , " " ) {
372+ return fmt .Errorf ("source name must not contain spaces" )
373+ }
374+
375+ // Build the source document
376+ document := workflow.Document {
377+ Location : workflow .LocationString (flags .Location ),
378+ }
379+
380+ // Add authentication if provided
381+ if flags .AuthHeader != "" {
382+ document .Auth = & workflow.Auth {
383+ Header : flags .AuthHeader ,
384+ Secret : "$openapi_doc_auth_token" ,
385+ }
386+ }
387+
388+ // Build the source
389+ source := workflow.Source {
390+ Inputs : []workflow.Document {document },
391+ }
392+
393+ // Set output path if provided
394+ if flags .OutputPath != "" {
395+ source .Output = & flags .OutputPath
396+ }
397+
398+ // Validate the source
399+ if err := source .Validate (); err != nil {
400+ return errors .Wrap (err , "failed to validate source configuration" )
401+ }
402+
403+ // Add source to workflow
404+ workflowFile .Sources [flags .SourceName ] = source
405+
406+ // Validate the workflow
407+ if err := workflowFile .Validate (generate .GetSupportedTargetNames ()); err != nil {
408+ return errors .Wrap (err , "failed to validate workflow file" )
409+ }
410+
411+ // Ensure .speakeasy directory exists
412+ if _ , err := os .Stat (".speakeasy" ); os .IsNotExist (err ) {
413+ if err := os .MkdirAll (".speakeasy" , 0o755 ); err != nil {
414+ return err
415+ }
416+ }
417+
418+ // Save the workflow
419+ if err := workflow .Save (workingDir , workflowFile ); err != nil {
420+ return errors .Wrap (err , "failed to save workflow file" )
421+ }
422+
423+ // Print success message
424+ logger .Printf ("Successfully configured source %q with location %q\n " , flags .SourceName , flags .Location )
425+ if flags .OutputPath != "" {
426+ logger .Printf (" Output path: %s\n " , flags .OutputPath )
427+ }
428+ if flags .AuthHeader != "" {
429+ logger .Printf (" Auth header: %s (value from $OPENAPI_DOC_AUTH_TOKEN)\n " , flags .AuthHeader )
430+ }
431+
432+ return nil
433+ }
434+
294435func configureTarget (ctx context.Context , flags ConfigureTargetFlags ) error {
295436 workingDir , err := os .Getwd ()
296437 if err != nil {
@@ -312,6 +453,11 @@ func configureTarget(ctx context.Context, flags ConfigureTargetFlags) error {
312453 workflowFile .Targets = make (map [string ]workflow.Target )
313454 }
314455
456+ // Non-interactive mode: when --target-type and --source are provided
457+ if flags .TargetType != "" && flags .SourceID != "" {
458+ return configureTargetNonInteractive (ctx , workingDir , workflowFile , flags )
459+ }
460+
315461 existingTarget := ""
316462 if _ , ok := workflowFile .Targets [flags .ID ]; ok {
317463 existingTarget = flags .ID
@@ -452,6 +598,166 @@ func configureTarget(ctx context.Context, flags ConfigureTargetFlags) error {
452598 return nil
453599}
454600
601+ // configureTargetNonInteractive handles target configuration without interactive prompts.
602+ // This is used when --target-type and --source flags are both provided.
603+ func configureTargetNonInteractive (ctx context.Context , workingDir string , workflowFile * workflow.Workflow , flags ConfigureTargetFlags ) error {
604+ logger := log .From (ctx )
605+
606+ // Validate target type is supported
607+ supportedTargets := generate .GetSupportedTargetNames ()
608+ if ! slices .Contains (supportedTargets , flags .TargetType ) {
609+ return fmt .Errorf ("unsupported target type %q; supported types: %s" , flags .TargetType , strings .Join (supportedTargets , ", " ))
610+ }
611+
612+ // Validate source exists
613+ if _ , ok := workflowFile .Sources [flags .SourceID ]; ! ok {
614+ var sourceNames []string
615+ for name := range workflowFile .Sources {
616+ sourceNames = append (sourceNames , name )
617+ }
618+ return fmt .Errorf ("source %q not found; available sources: %s" , flags .SourceID , strings .Join (sourceNames , ", " ))
619+ }
620+
621+ // Default target name to target type if not provided
622+ targetName := flags .TargetName
623+ if targetName == "" {
624+ targetName = flags .TargetType
625+ }
626+
627+ // Validate target name doesn't already exist
628+ if _ , ok := workflowFile .Targets [targetName ]; ok {
629+ return fmt .Errorf ("a target with the name %q already exists" , targetName )
630+ }
631+
632+ // Validate target name format
633+ if strings .Contains (targetName , " " ) {
634+ return fmt .Errorf ("target name must not contain spaces" )
635+ }
636+
637+ // Build the target
638+ target := workflow.Target {
639+ Target : flags .TargetType ,
640+ Source : flags .SourceID ,
641+ }
642+
643+ // Set output directory if provided
644+ if flags .OutputDir != "" {
645+ target .Output = & flags .OutputDir
646+ }
647+
648+ // Validate the target
649+ if err := target .Validate (supportedTargets , workflowFile .Sources ); err != nil {
650+ return errors .Wrap (err , "failed to validate target configuration" )
651+ }
652+
653+ // Add target to workflow
654+ workflowFile .Targets [targetName ] = target
655+
656+ // Build config for target
657+ targetConfig , err := config .GetDefaultConfig (true , generate .GetLanguageConfigDefaults , map [string ]bool {flags .TargetType : true })
658+ if err != nil {
659+ return errors .Wrapf (err , "failed to generate config for target %s" , targetName )
660+ }
661+
662+ // Set SDK class name if provided
663+ if flags .SDKClassName != "" {
664+ targetConfig .Generation .SDKClassName = flags .SDKClassName
665+ }
666+
667+ // Set base server URL if provided
668+ if flags .BaseServerURL != "" {
669+ targetConfig .Generation .BaseServerURL = flags .BaseServerURL
670+ }
671+
672+ // Set package name if provided
673+ if flags .PackageName != "" {
674+ if langConfig , ok := targetConfig .Languages [flags .TargetType ]; ok {
675+ if langConfig .Cfg == nil {
676+ langConfig .Cfg = make (map [string ]interface {})
677+ }
678+ // Different languages use different config keys for package name
679+ switch flags .TargetType {
680+ case "go" :
681+ langConfig .Cfg ["modulePath" ] = flags .PackageName
682+ case "java" :
683+ // For Java, split packageName into groupID and artifactID if it contains a colon
684+ if strings .Contains (flags .PackageName , ":" ) {
685+ parts := strings .SplitN (flags .PackageName , ":" , 2 )
686+ langConfig .Cfg ["groupID" ] = parts [0 ]
687+ langConfig .Cfg ["artifactID" ] = parts [1 ]
688+ } else {
689+ langConfig .Cfg ["groupID" ] = flags .PackageName
690+ }
691+ default :
692+ langConfig .Cfg ["packageName" ] = flags .PackageName
693+ }
694+ targetConfig .Languages [flags .TargetType ] = langConfig
695+ }
696+ }
697+
698+ // Determine output directory
699+ outDir := workingDir
700+ if target .Output != nil {
701+ outDir = * target .Output
702+ }
703+
704+ // Ensure .speakeasy directory exists in output dir
705+ if _ , err := os .Stat (filepath .Join (outDir , ".speakeasy" )); os .IsNotExist (err ) {
706+ if err := os .MkdirAll (filepath .Join (outDir , ".speakeasy" ), 0o755 ); err != nil {
707+ return err
708+ }
709+ }
710+
711+ // Create empty gen.yaml if it doesn't exist
712+ genYamlPath := filepath .Join (outDir , ".speakeasy/gen.yaml" )
713+ if _ , err := os .Stat (genYamlPath ); os .IsNotExist (err ) {
714+ if err := os .WriteFile (genYamlPath , []byte {}, 0o644 ); err != nil {
715+ return err
716+ }
717+ }
718+
719+ // Save config
720+ if err := config .SaveConfig (outDir , targetConfig ); err != nil {
721+ return errors .Wrapf (err , "failed to save config for target %s" , targetName )
722+ }
723+
724+ // Validate the workflow
725+ if err := workflowFile .Validate (supportedTargets ); err != nil {
726+ return errors .Wrap (err , "failed to validate workflow file" )
727+ }
728+
729+ // Ensure .speakeasy directory exists in working dir
730+ if _ , err := os .Stat (".speakeasy" ); os .IsNotExist (err ) {
731+ if err := os .MkdirAll (".speakeasy" , 0o755 ); err != nil {
732+ return err
733+ }
734+ }
735+
736+ // Save the workflow
737+ if err := workflow .Save (workingDir , workflowFile ); err != nil {
738+ return errors .Wrap (err , "failed to save workflow file" )
739+ }
740+
741+ // Print success message
742+ logger .Printf ("Successfully configured target %q\n " , targetName )
743+ logger .Printf (" Type: %s\n " , flags .TargetType )
744+ logger .Printf (" Source: %s\n " , flags .SourceID )
745+ if flags .OutputDir != "" {
746+ logger .Printf (" Output: %s\n " , flags .OutputDir )
747+ }
748+ if flags .SDKClassName != "" {
749+ logger .Printf (" SDK Class Name: %s\n " , flags .SDKClassName )
750+ }
751+ if flags .PackageName != "" {
752+ logger .Printf (" Package Name: %s\n " , flags .PackageName )
753+ }
754+ if flags .BaseServerURL != "" {
755+ logger .Printf (" Base Server URL: %s\n " , flags .BaseServerURL )
756+ }
757+
758+ return nil
759+ }
760+
455761func configurePublishing (ctx context.Context , flags ConfigureGithubFlags ) error {
456762 logger := log .From (ctx )
457763
0 commit comments