1515package sandbox
1616
1717import (
18- "encoding/json"
1918 "fmt"
2019 "strconv"
2120 "strings"
2221 "time"
2322
2423 "github.com/slackapi/slack-cli/internal/shared"
25- "github.com/slackapi/slack-cli/internal/shared/types"
2624 "github.com/slackapi/slack-cli/internal/slackerror"
2725 "github.com/slackapi/slack-cli/internal/style"
2826 "github.com/spf13/cobra"
@@ -36,25 +34,21 @@ type createFlags struct {
3634 owningOrgID string
3735 template string
3836 eventCode string
39- ttl string
40- autoLogin bool
41- output string
42- token string
37+ archiveTTL string // TTL duration, e.g. 1d, 2h
38+ archiveDate string // explicit date yyyy-mm-dd
4339}
4440
4541var createCmdFlags createFlags
4642
4743func NewCreateCommand (clients * shared.ClientFactory ) * cobra.Command {
4844 cmd := & cobra.Command {
4945 Use : "create [flags]" ,
50- Short : "Create a new sandbox" ,
51- Long : `Create a new Slack developer sandbox.
52-
53- Provisions a new sandbox. Domain is derived from org name if --domain is not provided.` ,
46+ Short : "Create a developer sandbox" ,
47+ Long : `Create a new Slack developer sandbox` ,
5448 Example : style .ExampleCommandsf ([]style.ExampleCommand {
55- {Command : "sandbox create --name test-box" , Meaning : "Create a sandbox named test-box" },
56- {Command : "sandbox create --name test-box --password mypass --owning-org-id E12345 " , Meaning : "Create a sandbox with login password and owning org " },
57- {Command : "sandbox create --name test-box --domain test-box --ttl 24h --output json " , Meaning : "Create an ephemeral sandbox for CI/CD with JSON output " },
49+ {Command : "sandbox create --name test-box --password mypass " , Meaning : "Create a sandbox named test-box" },
50+ {Command : "sandbox create --name test-box --password mypass --domain test-box --archive-ttl 1d " , Meaning : "Create a temporary sandbox that will be archived in 1 day " },
51+ {Command : "sandbox create --name test-box --password mypass -- domain test-box --archive-date 2025-12-31 " , Meaning : "Create a sandbox that will be archived on a specific date " },
5852 }),
5953 Args : cobra .NoArgs ,
6054 PreRunE : func (cmd * cobra.Command , args []string ) error {
@@ -68,79 +62,80 @@ Provisions a new sandbox. Domain is derived from org name if --domain is not pro
6862 cmd .Flags ().StringVar (& createCmdFlags .name , "name" , "" , "Organization name for the new sandbox" )
6963 cmd .Flags ().StringVar (& createCmdFlags .domain , "domain" , "" , "Team domain (e.g., pizzaknifefight). If not provided, derived from org name" )
7064 cmd .Flags ().StringVar (& createCmdFlags .password , "password" , "" , "Password used to log into the sandbox" )
71- cmd .Flags ().StringVar (& createCmdFlags .owningOrgID , "owning-org-id " , "" , "Enterprise team ID that manages your developer account, if applicable " )
65+ cmd .Flags ().StringVar (& createCmdFlags .locale , "locale " , "" , "Locale (eg. en-us, languageCode-countryCode) " )
7266 cmd .Flags ().StringVar (& createCmdFlags .template , "template" , "" , "Template ID for pre-defined data to preload" )
7367 cmd .Flags ().StringVar (& createCmdFlags .eventCode , "event-code" , "" , "Event code for the sandbox" )
74- cmd .Flags ().StringVar (& createCmdFlags .ttl , "ttl" , "" , "Time-to-live duration; sandbox will be archived after this period (e.g., 2h, 1d, 7d)" )
75- cmd .Flags ().StringVar (& createCmdFlags .output , "output" , "text" , "Output format: json, text" )
76- cmd .Flags ().StringVar (& createCmdFlags .token , "token" , "" , "Service account token for CI/CD authentication" )
68+ cmd .Flags ().StringVar (& createCmdFlags .archiveTTL , "archive-ttl" , "" , "Time-to-live duration; sandbox will be archived at end of day after this period (e.g., 2h, 1d, 7d)" )
69+ cmd .Flags ().StringVar (& createCmdFlags .archiveDate , "archive-date" , "" , "Explicit archive date in yyyy-mm-dd format. Cannot be used with --archive" )
7770
78- cmd .MarkFlagRequired ("name" )
79- cmd .MarkFlagRequired ("domain" )
80- cmd .MarkFlagRequired ("password" )
71+ // If one's developer account is managed by multiple Production Slack teams, one of those team IDs must be provided in the command
72+ cmd .Flags ().StringVar (& createCmdFlags .owningOrgID , "owning-org-id" , "" , "Enterprise team ID that manages your developer account, if applicable" )
73+
74+ if err := cmd .MarkFlagRequired ("name" ); err != nil {
75+ panic (err )
76+ }
77+ if err := cmd .MarkFlagRequired ("password" ); err != nil {
78+ panic (err )
79+ }
8180
8281 return cmd
8382}
8483
8584func runCreateCommand (cmd * cobra.Command , clients * shared.ClientFactory ) error {
8685 ctx := cmd .Context ()
8786
88- token , err := getSandboxToken (ctx , clients , createCmdFlags . token )
87+ auth , err := getSandboxAuth (ctx , clients )
8988 if err != nil {
9089 return err
9190 }
9291
9392 domain := createCmdFlags .domain
9493 if domain == "" {
95- domain = slugFromsandboxName (createCmdFlags .name )
94+ domain = domainFromName (createCmdFlags .name )
9695 }
9796
98- archiveDate , err := ttlToArchiveDate (createCmdFlags .ttl )
99- if err != nil {
100- return err
97+ if createCmdFlags .archiveTTL != "" && createCmdFlags .archiveDate != "" {
98+ return slackerror .New (slackerror .ErrInvalidArguments ).
99+ WithMessage ("Cannot use both --archive-ttl and --archive-date" ).
100+ WithRemediation ("Use only one: --archive-ttl for TTL (e.g., 3d) or --archive-date for a specific date (yyyy-mm-dd)" )
101+ }
102+
103+ archiveEpochDatetime := int64 (0 )
104+ if createCmdFlags .archiveTTL != "" {
105+ archiveEpochDatetime , err = getEpochFromTTL (createCmdFlags .archiveTTL )
106+ if err != nil {
107+ return err
108+ }
109+ } else if createCmdFlags .archiveDate != "" {
110+ archiveEpochDatetime , err = getEpochFromDate (createCmdFlags .archiveDate )
111+ if err != nil {
112+ return err
113+ }
101114 }
102115
103- result , err := clients .API ().CreateSandbox (ctx , token ,
116+ teamID , sandboxURL , err := clients .API ().CreateSandbox (ctx , auth . Token ,
104117 createCmdFlags .name ,
105118 domain ,
106119 createCmdFlags .password ,
107120 createCmdFlags .locale ,
108121 createCmdFlags .owningOrgID ,
109122 createCmdFlags .template ,
110123 createCmdFlags .eventCode ,
111- archiveDate ,
124+ archiveEpochDatetime ,
112125 )
113126 if err != nil {
114127 return err
115128 }
116129
117- switch createCmdFlags .output {
118- case "json" :
119- encoder := json .NewEncoder (clients .IO .WriteOut ())
120- encoder .SetIndent ("" , " " )
121- if err := encoder .Encode (result ); err != nil {
122- return err
123- }
124- default :
125- printCreateSuccess (cmd , clients , result )
126- }
127-
128- if createCmdFlags .autoLogin && result .URL != "" {
129- clients .Browser ().OpenURL (result .URL )
130- }
130+ printCreateSuccess (cmd , clients , teamID , sandboxURL )
131131
132132 return nil
133133}
134134
135- const maxTTL = 180 * 24 * time .Hour // 6 months
136-
137- // ttlToArchiveDate parses a TTL string (e.g., "24h", "1d", "7d") and returns the Unix epoch
138- // when the sandbox will be archived. Returns 0 if ttl is empty (no archiving). Supports
139- // Go duration format (h, m, s) and "Nd" for days. TTL cannot exceed 6 months.
140- func ttlToArchiveDate (ttl string ) (int64 , error ) {
141- if ttl == "" {
142- return 0 , nil
143- }
135+ // getEpochFromTTL parses a time-to-live string (e.g., "24h", "1d", "7d") and returns the Unix epoch
136+ // when the sandbox will be archived. Supports Go duration format (h, m, s) and "Nd" for days.
137+ // The value cannot exceed 6 months.
138+ func getEpochFromTTL (ttl string ) (int64 , error ) {
144139 var d time.Duration
145140 if strings .HasSuffix (strings .ToLower (ttl ), "d" ) {
146141 numStr := strings .TrimSuffix (strings .ToLower (ttl ), "d" )
@@ -160,16 +155,23 @@ func ttlToArchiveDate(ttl string) (int64, error) {
160155 WithRemediation ("Use a duration like 2h, 1d, or 7d" )
161156 }
162157 }
163- if d > maxTTL {
158+ return time .Now ().Add (d ).Unix (), nil
159+ }
160+
161+ // getEpochFromDate parses a date in yyyy-mm-dd format and returns the Unix epoch at start of that day (UTC).
162+ func getEpochFromDate (dateStr string ) (int64 , error ) {
163+ dateFormat := "2006-01-02"
164+ t , err := time .ParseInLocation (dateFormat , dateStr , time .UTC )
165+ if err != nil {
164166 return 0 , slackerror .New (slackerror .ErrInvalidArguments ).
165- WithMessage ("TTL cannot exceed 6 months" ).
166- WithRemediation ("Use a shorter duration (e.g., 2h, 1d, 7d )" )
167+ WithMessage ("Invalid archive date: %q" , dateStr ).
168+ WithRemediation ("Use yyyy-mm-dd format (e.g., 2025-12-31 )" )
167169 }
168- return time . Now (). Add ( d ) .Unix (), nil
170+ return t .Unix (), nil
169171}
170172
171- // slugFromsandboxName derives a domain-safe slug from org name (lowercase, alphanumeric + hyphens).
172- func slugFromsandboxName (name string ) string {
173+ // domainFromName derives domain-safe text from the name of the sandbox (lowercase, alphanumeric + hyphens).
174+ func domainFromName (name string ) string {
173175 var b []byte
174176 for _ , r := range name {
175177 if (r >= 'a' && r <= 'z' ) || (r >= '0' && r <= '9' ) {
@@ -195,15 +197,15 @@ func slugFromsandboxName(name string) string {
195197 return string (b )
196198}
197199
198- func printCreateSuccess (cmd * cobra.Command , clients * shared.ClientFactory , result types. CreateSandboxResult ) {
200+ func printCreateSuccess (cmd * cobra.Command , clients * shared.ClientFactory , teamID , url string ) {
199201 ctx := cmd .Context ()
200202 clients .IO .PrintInfo (ctx , false , "\n %s" , style .Sectionf (style.TextSection {
201203 Emoji : "beach_with_umbrella" ,
202204 Text : " Sandbox Created" ,
203205 Secondary : []string {
204- fmt .Sprintf ("Team ID: %s" , result .TeamID ),
205- fmt .Sprintf ("User ID: %s" , result .UserID ),
206- fmt .Sprintf ("URL: %s" , result .URL ),
206+ fmt .Sprintf ("Team ID: %s" , teamID ),
207+ fmt .Sprintf ("URL: %s" , url ),
207208 },
208209 }))
210+ clients .IO .PrintInfo (ctx , false , "Manage this sandbox from the CLI or visit\n %s" , style .Secondary ("https://api.slack.com/developer-program/sandboxes" ))
209211}
0 commit comments