@@ -15,9 +15,12 @@ import (
15
15
"github.com/superfly/flyctl/internal/cmdutil"
16
16
"github.com/superfly/flyctl/internal/command/launch/plan"
17
17
"github.com/superfly/flyctl/internal/flag"
18
+ "github.com/superfly/flyctl/internal/flyerr"
19
+ "github.com/superfly/flyctl/internal/haikunator"
18
20
"github.com/superfly/flyctl/internal/prompt"
19
21
"github.com/superfly/flyctl/iostreams"
20
22
"github.com/superfly/flyctl/scanner"
23
+ "github.com/superfly/graphql"
21
24
)
22
25
23
26
// Cache values between buildManifest and stateFromManifest
@@ -31,6 +34,14 @@ type planBuildCache struct {
31
34
srcInfo * scanner.SourceInfo
32
35
}
33
36
37
+ func appNameTakenErr (appName string ) error {
38
+ return flyerr.GenericErr {
39
+ Err : fmt .Sprintf ("app name %s is already taken" , appName ),
40
+ Descript : "each Fly.io app must have a unique name" ,
41
+ Suggest : "Please specify a different app name with --name" ,
42
+ }
43
+ }
44
+
34
45
func buildManifest (ctx context.Context ) (* LaunchManifest , * planBuildCache , error ) {
35
46
36
47
appConfig , copiedConfig , err := determineBaseAppConfig (ctx )
@@ -183,6 +194,10 @@ func stateFromManifest(ctx context.Context, m LaunchManifest, optionalCache *pla
183
194
}
184
195
}
185
196
197
+ if taken , _ := appNameTaken (ctx , appConfig .AppName ); taken {
198
+ return nil , appNameTakenErr (appConfig .AppName )
199
+ }
200
+
186
201
workingDir := flag .GetString (ctx , "path" )
187
202
if absDir , err := filepath .Abs (workingDir ); err == nil {
188
203
workingDir = absDir
@@ -274,9 +289,39 @@ func determineAppName(ctx context.Context, configPath string) (string, string, e
274
289
if appName == "" {
275
290
return "" , "" , errors .New ("enable to determine app name, please specify one with --name" )
276
291
}
292
+ // If the app name is already taken, try to generate a unique suffix.
293
+ if taken , _ := appNameTaken (ctx , appName ); taken {
294
+ delimiter := "-"
295
+ var newName string
296
+ found := false
297
+ for i := 1 ; i < 10 ; i ++ {
298
+ newName = fmt .Sprintf ("%s%s%s" , appName , delimiter , haikunator .Haikunator ().Delimiter (delimiter ))
299
+ if taken , _ := appNameTaken (ctx , newName ); ! taken {
300
+ found = true
301
+ break
302
+ }
303
+ }
304
+ if ! found {
305
+ return "" , "" , appNameTakenErr (appName )
306
+ }
307
+ appName = newName
308
+ }
277
309
return appName , "derived from your directory name" , nil
278
310
}
279
311
312
+ func appNameTaken (ctx context.Context , name string ) (bool , error ) {
313
+ client := client .FromContext (ctx ).API ()
314
+ _ , err := client .GetAppBasic (ctx , name )
315
+ if err != nil {
316
+ if api .IsNotFoundError (err ) || graphql .IsNotFoundError (err ) {
317
+ return false , nil
318
+ }
319
+ return false , err
320
+ }
321
+
322
+ return true , nil
323
+ }
324
+
280
325
// determineOrg returns the org specified on the command line, or the personal org if left unspecified
281
326
func determineOrg (ctx context.Context ) (* api.Organization , string , error ) {
282
327
var (
0 commit comments