public void NewApp(
            Settings settings,
            string appName,
            string rootNamespace,
            string workingDirectory,
            string moduleFile
            )
        {
            // validate name
            if (!IsValidResourceName(appName))
            {
                LogError("app name is not valid");
                return;
            }

            // parse yaml module definition
            if (!File.Exists(moduleFile))
            {
                LogError($"could not find module '{moduleFile}'");
                return;
            }
            var moduleContents = File.ReadAllText(moduleFile);
            var module         = new ModelYamlToAstConverter(new Settings(Version), moduleFile).Convert(moduleContents, selector: null);

            if (HasErrors)
            {
                return;
            }

            // set default namespace if none is set
            if (rootNamespace == null)
            {
                rootNamespace = $"{module.Module}.{appName}";
            }

            // create directory for app project
            var projectDirectory = Path.Combine(workingDirectory, appName);

            if (Directory.Exists(projectDirectory))
            {
                LogError($"project directory '{projectDirectory}' already exists");
                return;
            }
            try {
                Directory.CreateDirectory(projectDirectory);
            } catch (Exception e) {
                LogError($"unable to create directory '{projectDirectory}'", e);
                return;
            }

            // generate project
            var substitutions = new Dictionary <string, string> {
                ["ROOTNAMESPACE"] = rootNamespace,
                ["APPNAME"]       = appName
            };

            using (var projectStream = typeof(CliNewCommand).Assembly.GetManifestResourceStream("LambdaSharp.Tool.Resources.BlazorProjectTemplate.zip"))
                using (var projectArchive = new ZipArchive(projectStream)) {
                    foreach (var entry in projectArchive.Entries)
                    {
                        var entryPath = Path.Combine(projectDirectory, entry.FullName);
                        if (entry.FullName.EndsWith("/"))
                        {
                            Directory.CreateDirectory(entryPath);
                        }
                        else
                        {
                            // use app name for .csproj file
                            entryPath = (entry.Name == "MyApp._csproj")
                            ? Path.Combine(projectDirectory, $"{appName}.csproj")
                            : entryPath;
                            using (var file = File.OpenWrite(entryPath))
                                using (var entryStream = entry.Open()) {
                                    var fileExtension = Path.GetExtension(entryPath);
                                    switch (fileExtension)
                                    {
                                    case ".cs":
                                    case ".csproj":
                                    case ".razor":
                                    case ".html":

                                        // apply substitutions to source files
                                        using (var entryReader = new StreamReader(entryStream)) {
                                            var contents = entryReader.ReadToEnd();
                                            foreach (var kv in substitutions)
                                            {
                                                contents = contents.Replace($"%%{kv.Key}%%", kv.Value);
                                            }
                                            var contentsBytes = Encoding.UTF8.GetBytes(contents);
                                            file.Write(contentsBytes, 0, contentsBytes.Length);
                                        }
                                        break;

                                    default:

                                        // copy zip contents verbatim
                                        entryStream.CopyTo(file);
                                        break;
                                    }
                                }
                            Console.WriteLine($"Created file: {Path.GetRelativePath(Directory.GetCurrentDirectory(), entryPath)}");
                        }
                    }
                }

            // insert app definition and a variable to output the app website URL
            InsertModuleItemsLines(moduleFile, new[] {
                $"  - App: {appName}",
                $"    Description: TO-DO - update app description"
            });
            InsertModuleItemsLines(moduleFile, new[] {
                $"  - Variable: {appName}WebsiteUrl",
                $"    Description: {appName} Website URL",
                $"    Scope: stack",
                $"    Value: !GetAtt {appName}::Bucket.Outputs.WebsiteUrl"
            });
        }
        public void NewFunction(
            Settings settings,
            string functionName,
            string rootNamespace,
            string framework,
            string workingDirectory,
            string moduleFile,
            string language,
            int functionMemory,
            int functionTimeout,
            FunctionType functionType
            )
        {
            // validate name
            if (!IsValidResourceName(functionName))
            {
                LogError("function name is not valid");
                return;
            }

            // parse yaml module definition
            if (!File.Exists(moduleFile))
            {
                LogError($"could not find module '{moduleFile}'");
                return;
            }
            var moduleContents = File.ReadAllText(moduleFile);
            var module         = new ModelYamlToAstConverter(new Settings(Version), moduleFile).Convert(moduleContents, selector: null);

            if (HasErrors)
            {
                return;
            }

            // set default namespace if none is set
            if (rootNamespace == null)
            {
                rootNamespace = $"{module.Module}.{functionName}";
            }

            // create directory for function project
            var projectDirectory = Path.Combine(workingDirectory, functionName);

            if (Directory.Exists(projectDirectory))
            {
                LogError($"project directory '{projectDirectory}' already exists");
                return;
            }
            try {
                Directory.CreateDirectory(projectDirectory);
            } catch (Exception e) {
                LogError($"unable to create directory '{projectDirectory}'", e);
                return;
            }

            // create function file
            switch (language)
            {
            case "csharp":
                NewCSharpFunction(
                    settings,
                    functionName,
                    rootNamespace,
                    framework,
                    workingDirectory,
                    moduleFile,
                    functionMemory,
                    functionTimeout,
                    projectDirectory,
                    functionType
                    );
                break;

            case "javascript":
                NewJavascriptFunction(
                    settings,
                    functionName,
                    rootNamespace,
                    framework,
                    workingDirectory,
                    moduleFile,
                    functionMemory,
                    functionTimeout,
                    projectDirectory,
                    functionType
                    );
                break;
            }

            // insert function definition
            InsertModuleItemsLines(moduleFile, new[] {
                $"  - Function: {functionName}",
                $"    Description: TO-DO - update function description",
                $"    Memory: {functionMemory}",
                $"    Timeout: {functionTimeout}"
            });
        }