async function processConfiguration()

in packages/MSBot/src/msbot-clone-services.ts [148:891]


async function processConfiguration(): Promise<void> {
    if (!args.sdkVersion) {
        args.sdkVersion = "v4";
    }

    if (!args.sdkLanguage) {
        if (fs.existsSync("package.json")) {
            args.sdkLanguage = "Node";
        } else {
            args.sdkLanguage = "CSharp";
        }
    }

    if (!args.groupName) {
        args.groupName = args.name;
    }

    if ((<any>args)['proj-file']) {
        args.projFile = (<string>(<any>args)['proj-file']);
        console.log(args.projFile);
    }
    else if ((<any>args)['code-dir']) {
        args.codeDir = (<string>(<any>args)['code-dir']);
        console.log(args.codeDir);
    }

    if (!args.projFile && !args.codeDir) {
        let files = fs.readdirSync('.');
        for (let file of files) {
            if (path.extname(file) == '.csproj') {
                args.projFile = file;
                break;
            }
        }
    }

    if (!args.searchSku) {
        args.searchSku = "basic";
    }

    // verify az command exists and is correct version
    await checkAzBotServiceVersion();

    if (args.projFile) {
        await checkDotNetRequirement();
    }

    let recipe = <BotRecipe> await fsExtra.readJson(path.join(args.folder, `bot.recipe`));

    // get subscription account data
    let command: string = `az account show `;
    if (args.subscriptionId) {
        command += `--subscription ${args.subscriptionId}`;
    }

    let azAccount = await runCommand(command, `Fetching subscription account`);
    args.subscriptionName = azAccount.name;
    args.subscriptionId = azAccount.id;
    args.tenantId = azAccount.tenantId;

    try {
        // pass 0 - tell the user what are going to create
        if (!args.quiet) {
            let bot = 0;
            let appInsights = 0;
            let storage = 0;
            let sitePlan = 0;
            console.log("The following services will be created by this operation:");

            const table = new Table({
                // don't use lines for table
                chars: {
                    'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
                    'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
                    'left': '', 'left-mid': '', 'right': '', 'right-mid': '',
                    'mid': '', 'mid-mid': '', 'middle': ''
                },
                head: [chalk.default.bold('Service'), chalk.default.bold('Location'), chalk.default.bold('SKU'), chalk.default.bold("Resource Group")],
                colWidths: [40, 20, 20, 20],
                style: { 'padding-left': 1, 'padding-right': 1 },
                wordWrap: true
            });

            let hasSitePlan = false;
            let rows = [];
            for (let resource of recipe.resources) {
                switch (resource.type) {
                    case ServiceTypes.AppInsights:
                        let appInsightsRegion = regionToAppInsightRegionNameMap[args.location];
                        if (appInsightsRegion) {
                            rows.push([`Azure AppInsights Service`, `${appInsightsRegion}`, `F0`, args.groupName]);
                        }
                        break;

                    case ServiceTypes.BlobStorage:
                        rows.push(['Azure Blob Storage Service', `${args.location}`, 'Standard_LRS', args.groupName]);
                        break;

                    case ServiceTypes.Bot:
                        rows.push([`Azure Bot Service Registration`, `Global`, ``, args.groupName]);
                        if (!hasSitePlan) {
                            rows.push([`Azure App Site Plan`, `${args.location}`, `S1`, args.groupName]);
                            hasSitePlan = true;
                        }
                        rows.push([`Azure WebApp Service (Bot)`, `${args.location}`, ``, args.groupName]);
                        break;

                    case ServiceTypes.CosmosDB:
                        rows.push([`Azure CosmosDB Service`, `${args.location}`, `1 write region`, args.groupName]);
                        break;

                    case ServiceTypes.Endpoint:
                        break;

                    case ServiceTypes.File:
                        break;

                    case ServiceTypes.Generic:
                        break;

                    case ServiceTypes.Dispatch:
                    case ServiceTypes.Luis:
                        if (!args.luisAuthoringKey) {
                            throw new Error('missing --luisAuthoringKey argument');
                        }

                        if (!args.luisPublishRegion) {
                            args.luisPublishRegion = luisPublishRegions.find((value) => value == args.location);
                            if (!args.luisPublishRegion) {
                                args.luisPublishRegion = regionToLuisPublishRegionMap[args.location];
                            }
                        }

                        if (resource.type == ServiceTypes.Dispatch)
                            rows.push([`Azure LUIS Cognitive Service (Dispatch)`, `${args.luisPublishRegion}`, `S0`, args.groupName]);
                        else
                            rows.push([`Azure LUIS Cognitive Service`, `${args.luisPublishRegion}`, `S0`, args.groupName]);
                        break;

                    case ServiceTypes.QnA:
                        rows.push([`Azure QnA Maker Service`, `westus`, `S0`, args.groupName]);
                        if (!hasSitePlan) {
                            rows.push([`Azure App Site Plan`, `${args.location}`, `S1`, args.groupName]);
                            hasSitePlan = true;
                        }
                        rows.push([`Azure WebApp Service (QnA)`, `${args.location}`, ``, args.groupName]);
                        rows.push([`Azure Search Service`, `${regionToSearchRegionMap[args.location]}`, args.searchSku, args.groupName]);
                        break;

                    default:
                        break;
                }
            }
            rows.sort((a, b) => a[0].localeCompare(b[0]));
            for (let row of rows)
                table.push(row);
            await logAsync(table.toString());

            console.log(`Resources will be created in subscription: ${chalk.default.bold(azAccount.name)} (${azAccount.id})`);
            if (!args.force) {
                const answer = readline.question(`Would you like to perform this operation? [y/n]`);
                if (answer == "no" || answer == "n") {
                    // remove orphaned bot file if it exists
                    if (fs.existsSync(args.name + '.bot')) fs.unlinkSync(args.name + '.bot');
                    console.log("Canceling the operation");
                    process.exit(1);
                }
            }
        }

        // pass 1 - create bot if we are going to need one.  This will create 
        // * group
        // * sitePlan
        // * site
        // * appInsights
        // * storage
        // create group
        let azGroup: any;
        let azBot: IBotService | undefined;
        let azSitePlan: any;
        let storageInfo;
        let azQnaSubscription: any;
        let azLuisSubscription: any;
        let azAppInsights: any;
        let azBotExtended: any;
        let azBotEndpoint: IEndpointService | undefined;


        // create group if not created yet
        azGroup = await createGroup();

        for (let resource of recipe.resources) {
            if (resource.type == ServiceTypes.Bot) {
                if (!azBot) {
                    azBot = await createBot();
                    azBotEndpoint = <IEndpointService><any>azBot;

                    azBotExtended = await runCommand(`az bot show -g ${args.groupName} -n ${args.name} --subscription ${args.subscriptionId}`,
                        `Fetching bot extended information [${args.name}]`);

                    // fetch co-created resources so we can get blob and appinsights data
                    let azGroupResources = await runCommand(`az resource list -g ${azGroup.name} --subscription ${args.subscriptionId}`,
                        `Fetching co-created resources [${args.name}]`);
                    let botWebSite;
                    for (let resource of azGroupResources) {
                        switch (resource.type) {
                            case "microsoft.insights/components":
                                azAppInsights = resource;
                                break;
                            case "Microsoft.Storage/storageAccounts":
                                storageInfo = resource;
                                break;
                            case "Microsoft.Web/serverfarms":
                                azSitePlan = resource;
                                break;
                            case "Microsoft.Web/sites":
                                // the website for the bot does have the bot name in it (qna host does)
                                if (resource.name.indexOf('-qnahost') < 0)
                                    botWebSite = resource;
                                break;
                        }
                    }
                    // get appSettings from botAppSite (specifically to get secret)
                    if (botWebSite) {                                
                        if(args.encryption){
                            let botAppSettings = await runCommand(`az webapp config appsettings list -g ${args.groupName} -n ${botWebSite.name} --subscription ${args.subscriptionId}`,
                            `Fetching bot website appsettings [${args.name}]`);
                            for (let setting of botAppSettings) {
                                if (setting.name == "botFileSecret") {
                                    args.secret = setting.value;
                                    break;
                                }
                            }

                            if (!args.secret) {
                                args.secret = BotConfiguration.generateKey();
                                // set the appsetting
                                await runCommand(`az webapp config appsettings set -g ${args.groupName} -n ${botWebSite.name} --settings botFileSecret="${args.secret}" --subscription ${args.subscriptionId}`,
                                    `Setting bot website appsettings secret [${args.name}]`);
                            }                            
                        }

                    } else {
                        throw new Error('botsite was not found');
                    }

                    config.services.push(new BotService({
                        type: ServiceTypes.Bot,
                        id: resource.id,
                        name: azBot.name,
                        tenantId: args.tenantId,
                        subscriptionId: args.subscriptionId,
                        resourceGroup: args.groupName,
                        serviceName: azBot.name,
                        appId: azBot.appId
                    }));
                    await config.save();
                }
            }
        }

        // pass 2 - create LUIS and QNA cognitive service subscriptions (and hosting services)
        for (let resource of recipe.resources) {
            switch (resource.type) {
                case ServiceTypes.Luis:
                case ServiceTypes.Dispatch:
                    if (!azLuisSubscription) {
                        if (!args.luisAuthoringRegion) {
                            if (regionToLuisAuthoringRegionMap.hasOwnProperty(args.location))
                                args.luisAuthoringRegion = regionToLuisAuthoringRegionMap[args.location];
                            else
                                throw new Error(`${args.location} does not have a valid luisAuthoringRegion.  Pass --luisAuthoringRegion to tell us which region you are in`);
                        }

                        if (!args.luisPublishRegion) {
                            args.luisPublishRegion = luisPublishRegions.find((value) => value == args.location);
                            if (!args.luisPublishRegion) {
                                args.luisPublishRegion = regionToLuisAuthoringRegionMap[args.location];
                            }
                        }

                        // create luis subscription
                        let luisCogsName = `${args.name}-LUIS`;
                        azLuisSubscription = await runCommand(`az cognitiveservices account create -g ${azGroup.name} --kind LUIS -n "${luisCogsName}" --location ${args.luisPublishRegion} --sku S0 --yes --subscription ${args.subscriptionId}`,
                            `Creating LUIS Cognitive Service [${luisCogsName}]`);

                        // get keys
                        let luisKeys = await runCommand(`az cognitiveservices account keys list -g ${azGroup.name} -n "${luisCogsName}" --subscription ${args.subscriptionId}`,
                            `Fetching LUIS Keys [${luisCogsName}]`);
                        args.luisSubscriptionKey = luisKeys.key1;
                    }
                    break;

                case ServiceTypes.QnA:
                    if (!azQnaSubscription) {

                        if (!azSitePlan) {
                            azSitePlan = await runCommand(`az appservice plan create -g  ${args.groupName} --sku s1 --name ${args.name} --subscription ${args.subscriptionId}`,
                                `Creating site plan [${args.name}]`);
                        }
                        // create qnaMaker service in resource group

                        // we have a group, and app service, 

                        // provision search instance
                        let searchName = args.name.toLowerCase() + '-search';
                        let searchResult = await runCommand(`az search service create -g ${azGroup.name} -n "${searchName}" --location ${regionToSearchRegionMap[args.location]} --sku ${args.searchSku} --subscription ${args.subscriptionId}`,
                            `Creating Azure Search Service [${searchName}]`);

                        // get search keys
                        let searchKeys = await runCommand(`az search admin-key show -g ${azGroup.name} --service-name "${searchName}" --subscription ${args.subscriptionId}`,
                            `Fetching Azure Search Service keys [${searchName}]`);

                        // create qna host service
                        let qnaHostName = args.name + '-qnahost';
                        let createQnaWeb = await runCommand(`az webapp create -g ${azGroup.name} -n ${qnaHostName} --plan ${args.name} --subscription ${args.subscriptionId}`,
                            `Creating QnA Maker host web service [${qnaHostName}]`);

                        // configure qna web service settings
                        command = `az webapp config appsettings set -g ${azGroup.name} -n ${qnaHostName} --subscription ${args.subscriptionId} --settings `;
                        command += `"AzureSearchName=${searchName}" `;
                        command += `AzureSearchAdminKey=${searchKeys.primaryKey} `;
                        command += `PrimaryEndpointKey=${qnaHostName}-PrimaryEndpointKey  `;
                        command += `SecondaryEndpointKey=${qnaHostName}-SecondaryEndpointKey `;
                        command += `DefaultAnswer="No good match found in KB." `;
                        command += `QNAMAKER_EXTENSION_VERSION="latest"`;
                        await runCommand(command, `Configuring QnA Maker host web service settings [${qnaHostName}]`);

                        await runCommand(`az webapp cors add -g ${azGroup.name} -n ${qnaHostName} -a "*" --subscription ${args.subscriptionId}`,
                            `Configuring QnA Maker host web service CORS [${qnaHostName}]`);

                        // create qnamaker account
                        let qnaAccountName = args.name + '-QnAMaker';
                        command = `az cognitiveservices account create -g ${azGroup.name} --kind QnAMaker -n "${qnaAccountName}" --sku S0 --subscription ${args.subscriptionId} `;
                        // NOTE: currently qnamaker is only available in westus
                        command += `--location westus --yes `;
                        command += `--api-properties qnaRuntimeEndpoint=https://${qnaHostName}.azurewebsites.net`;
                        azQnaSubscription = await runCommand(command, `Creating QnA Maker Cognitive Service [${qnaAccountName}]`);

                        // get qna subscriptionKey
                        let azQnaKeys = await runCommand(`az cognitiveservices account keys list -g ${azGroup.name} -n "${qnaAccountName}" --subscription ${args.subscriptionId}`,
                            `Fetching QnA Maker Cognitive Service [${qnaAccountName}]`);
                        args.qnaSubscriptionKey = azQnaKeys.key1;
                    }
                    break;

                default:
                    if (!args.location) {
                        throw new Error('missing --location argument');
                    }
                    break;
            }
        }

        // pass 3- create the actual services
        for (let resource of recipe.resources) {

            switch (resource.type) {

                case ServiceTypes.AppInsights:
                    {
                        // this was created via az bot create, hook it up
                        if (azAppInsights && resource.id) {
                            let appInsights = await getAppInsightsService(azAppInsights);
                            appInsights.id = resource.id; // keep original id
                            config.services.push(appInsights);
                        } else {
                            console.warn("WARNING: No bot appInsights plan was created because no bot was created");
                        }
                        await config.save();
                    }
                    break;

                case ServiceTypes.BlobStorage:
                    {
                        // this was created via az bot create, get the connection string and then hook it up
                        if (!storageInfo) {
                            let storageName: string = `${azGroup.name.toLowerCase() + generateShortId()}storage`;
                            storageInfo = await runCommand(`az storage account create -g ${azGroup.name} -n "${storageName}" --location ${args.location} --sku Standard_LRS --subscription ${args.subscriptionId}`,
                                `Creating Azure Blob Storage  [${storageName}]`);
                        }

                        if (storageInfo) {
                            let blobConnection = await runCommand(`az storage account show-connection-string -g ${azGroup.name} -n "${storageInfo.name}" --subscription ${args.subscriptionId}`,
                                `Fetching Azure Blob Storage connection string [${args.name}]`);

                            let blobResource = <IBlobResource>resource;
                            config.services.push(new BlobStorageService({
                                type: ServiceTypes.BlobStorage,
                                id: resource.id,
                                name: storageInfo.name,
                                serviceName: storageInfo.name,
                                tenantId: args.tenantId,
                                subscriptionId: args.subscriptionId,
                                resourceGroup: args.groupName,
                                connectionString: blobConnection.connectionString,
                                container: blobResource.container
                            }));
                        }
                        await config.save();
                    }
                    break;

                case ServiceTypes.Bot:
                    {
                        // already created
                    }
                    break;

                case ServiceTypes.CosmosDB:
                    {
                        let cosmosResource = <ICosmosDBResource>resource;
                        let cosmosName = `${args.name.toLowerCase()}`;

                        // az cosmosdb create --n name -g Group1
                        let cosmosDb = await runCommand(`az cosmosdb create -n ${cosmosName} -g ${azGroup.name} --subscription ${args.subscriptionId}`,
                            `Creating Azure CosmosDB account [${cosmosName}] ${chalk.default.italic.yellow(`(Please be patient, this may take 5 minutes)`)}`);

                        // get keys
                        let cosmosDbKeys = await runCommand(`az cosmosdb list-keys -g ${azGroup.name} -n ${cosmosName} --subscription ${args.subscriptionId}`,
                            `Fetching Azure CosmosDB account keys [${args.name}]`);

                        // az cosmosdb database create -n clonebot1cosmosdb --key <key> -d db1 --url-connection https://clonebot1cosmosdb.documents.azure.com:443/
                        await runCommand(`az cosmosdb database create -g ${azGroup.name} -n ${cosmosName} --key ${cosmosDbKeys.primaryMasterKey} -d ${cosmosResource.database} --url-connection https://${cosmosName}.documents.azure.com:443/ --subscription ${args.subscriptionId}`,
                            `Creating Azure CosmosDB database [${cosmosResource.database}]`);

                        // az cosmosdb collection create -n clonebot1cosmosdb --key <key> -d db1 --url-connection https://clonebot1cosmosdb.documents.azure.com:443/ --collection-name collection
                        await runCommand(`az cosmosdb collection create -g ${azGroup.name} -n ${cosmosName} --key ${cosmosDbKeys.primaryMasterKey} -d ${cosmosResource.database} --url-connection https://${cosmosName}.documents.azure.com:443/ --collection-name ${cosmosResource.collection} --subscription ${args.subscriptionId}`,
                            `Creating Azure CosmosDB collection [${cosmosResource.collection}]`);

                        // get connection string is broken
                        // command = `az cosmosdb list-connection-strings -g ${azGroup.name} -n ${args.name}`;
                        // logCommand(args, `Fetching cosmosdb connection strings ${cosmosResource.collection}`, command);
                        // p = await exec(command);
                        // let connections = JSON.parse(p.stdout);

                        // register it as a service
                        config.services.push(new CosmosDbService({
                            type: ServiceTypes.CosmosDB,
                            id: cosmosResource.id,
                            name: cosmosName,
                            serviceName: cosmosName,
                            tenantId: args.tenantId,
                            subscriptionId: args.subscriptionId,
                            resourceGroup: args.groupName,
                            endpoint: `https://${cosmosName}.documents.azure.com:443/`,
                            key: cosmosDbKeys.primaryMasterKey,
                            database: cosmosResource.database,
                            collection: cosmosResource.collection,
                        }));
                    }
                    await config.save();
                    break;

                case ServiceTypes.Endpoint:
                    {
                        let urlResource = <IUrlResource>resource;
                        if (urlResource.url && urlResource.url.indexOf('localhost') > 0) {
                            // add localhost record as is, but add appId/password
                            config.services.push(new EndpointService({
                                type: ServiceTypes.Endpoint,
                                id: resource.id,
                                name: resource.name,
                                appId: (azBotEndpoint) ? azBotEndpoint.appId : '',
                                appPassword: (azBotEndpoint) ? azBotEndpoint.appPassword : '',
                                endpoint: urlResource.url
                            }));
                        } else {
                            // merge oldUrl and new Url hostname
                            let oldUrl = new url.URL(urlResource.url);
                            if (azBotEndpoint) {

                                let azUrl = new url.URL(azBotEndpoint.endpoint);
                                oldUrl.hostname = azUrl.hostname;

                                config.services.push(new EndpointService({
                                    type: ServiceTypes.Endpoint,
                                    id: resource.id,
                                    name: resource.name,
                                    appId: azBotEndpoint.appId,
                                    appPassword: azBotEndpoint.appPassword,
                                    endpoint: oldUrl.href
                                }));

                                if (oldUrl != azUrl) {
                                    // TODO update bot service record with merged url
                                }
                            } else {
                                console.warn("There is no cloud endpoint to because there is no bot created");
                            }
                        }
                        await config.save();
                    }
                    break;

                case ServiceTypes.File:
                    {
                        let fileResource = <IFileResource>resource;
                        config.services.push(new FileService({
                            type: ServiceTypes.File,
                            id: fileResource.id,
                            name: fileResource.name,
                            path: fileResource.path,
                        }));
                        await config.save();
                    }
                    break;

                case ServiceTypes.Generic:
                    {
                        let genericResource = <IGenericResource>resource;
                        config.services.push(new GenericService({
                            type: ServiceTypes.Generic,
                            id: genericResource.id,
                            name: genericResource.name,
                            url: genericResource.url,
                            configuration: genericResource.configuration,
                        }));
                        await config.save();
                    }
                    break;

                case ServiceTypes.Dispatch:
                    {
                        let luisService = await importAndTrainLuisApp(resource);
                        let dispatchResource = <IDispatchResource>resource;
                        let dispatchService: IDispatchService = Object.assign({ serviceIds: dispatchResource.serviceIds, }, luisService);
                        (<any>dispatchService).type = ServiceTypes.Dispatch;
                        dispatchService.id = resource.id; // keep same resource id
                        config.services.push(new DispatchService(dispatchService));
                        await config.save();
                    }
                    break;

                case ServiceTypes.Luis:
                    {
                        if (!commandExistsSync('luis')) {
                            console.error(chalk.default.redBright(`Unable to find LUIS CLI. Please install via npm i -g luis-apis and try again. \n\nSee https://aka.ms/msbot-clone-services for pre-requisites.`))
                            showErrorHelp();
                        }
                        let luisService = await importAndTrainLuisApp(resource);
                        luisService.id = `${resource.id}`; // keep same resource id
                        config.services.push(luisService);
                        await config.save();
                    }
                    break;

                case ServiceTypes.QnA:
                    {
                        if (!commandExistsSync('qnamaker')) {
                            console.error(chalk.default.redBright(`Unable to find QnAMaker CLI. Please install via npm i -g qnamaker and try again. \n\nSee https://aka.ms/msbot-clone-services for pre-requisites.`))
                            showErrorHelp();
                        }
                        let qnaPath = path.join(args.folder, `${resource.id}.qna`);
                        let kbName = resource.name;

                        // These values pretty much gaurantee success. We can decrease them if the QnA backend gets faster
                        const initialDelaySeconds = 30;
                        let retryAttemptsRemaining = 3;
                        let retryDelaySeconds = 15;
                        const retryDelayIncrease = 30;

                        console.log(`Waiting ${initialDelaySeconds} seconds for QnAMaker backend to finish setting up...`);
                        await sleep(initialDelaySeconds);

                        let svc;
                        while (retryAttemptsRemaining >= 0) {
                            try {
                                svc = await runCommand(`qnamaker create kb --subscriptionKey ${args.qnaSubscriptionKey} --name "${kbName}" --in ${qnaPath} --wait --msbot`,
                                    `Creating QnA Maker KB [${kbName}]`);
                                break;
                            } catch (err) {
                                // Helpful error messages are mostly in err.stderr, when available. err.stderr can't be parsed to JSON, so we have to search for substrings
                                if (err.stderr) {
                                    const generalFailedString = `"operationState\": \"Failed\"`;
                                    const invalidSubscriptionString = `Access denied due to invalid subscription key`;
                                    // Usually the first failure
                                    if (err.stderr.includes(invalidSubscriptionString)) {
                                        console.warn(chalk.default.yellowBright(`QnAMaker backend still generating API keys. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`));
                                    // Usually the remaining, non-breaking failures
                                    } else if (err.stderr.includes(generalFailedString)) {
                                        console.warn(chalk.default.yellowBright(`QnAMaker backend not ready. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`));
                                    } else {
                                        console.error(chalk.default.yellowBright(`QnAMaker doesn't seem to be working. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`));
                                    }
                                }
                                retryAttemptsRemaining--;
                                await sleep(retryDelaySeconds);
                                retryDelaySeconds += retryDelayIncrease;

                                if (retryAttemptsRemaining < 0) {
                                    console.error(chalk.default.redBright(`Unable to create QnA KB.`));
                                    showErrorHelp();
                                } else {
                                    continue;
                                }
                            }
                        }

                        let service = new QnaMakerService(svc);
                        service.id = `${resource.id}`; // keep id
                        service.name = kbName;
                        config.services.push(service);
                        await config.save();

                        // publish  
                        try {
                            await runCommand(`qnamaker publish kb --subscriptionKey ${service.subscriptionKey} --kbId ${service.kbId} --hostname ${service.hostname} --endpointKey ${service.endpointKey}`,
                                `Publishing QnA Maker KB [${kbName}]`);
                        } catch (err) {
                            console.warn(err.message || err);
                        }
                    }
                    break;

                default:
                    break;
            }
        }

        // hook up appinsights and blob storage if it hasn't been already
        if (azBot) {
            let hasBot = false;
            let hasBlob = false;
            let hasAppInsights = false;
            for (let service of config.services) {
                switch (service.type) {
                    case ServiceTypes.AppInsights:
                        hasAppInsights = true;
                        break;
                    case ServiceTypes.BlobStorage:
                        hasBlob = true;
                        break;
                    case ServiceTypes.Bot:
                        hasBot = true;
                        break;
                }
            }
            if (!hasBot) {
                // created via az bot create, register the result
                config.connectService(new BotService({
                    name: azBot.name,
                    tenantId: args.tenantId,
                    subscriptionId: args.subscriptionId,
                    resourceGroup: args.groupName,
                    serviceName: azBot.name,
                    appId: azBot.appId
                }));
            }

            if (azBotEndpoint) {
                // add endpoint
                config.connectService(new EndpointService({
                    type: ServiceTypes.Endpoint,
                    name: "production",
                    appId: azBotEndpoint.appId,
                    appPassword: azBotEndpoint.appPassword,
                    endpoint: azBotEndpoint.endpoint
                }));

                await config.save();
            }

            if (!hasAppInsights) {
                if (azAppInsights) {
                    let appInsights = await getAppInsightsService(azAppInsights);
                    config.connectService(appInsights);
                } else {
                    console.warn("WARNING: No bot appInsights plan was created because no bot was created");
                }
                await config.save();
            }

            if (!hasBlob && storageInfo) {
                // this was created via az bot create, get the connection string and then hook it up
                let blobConnection = await runCommand(`az storage account show-connection-string -g ${azGroup.name} -n "${storageInfo.name}" --subscription ${args.subscriptionId}`,
                    `Fetching storage connection string [${storageInfo.name}]`);

                config.connectService(new BlobStorageService({
                    name: storageInfo.name,
                    serviceName: storageInfo.name,
                    tenantId: args.tenantId,
                    subscriptionId: args.subscriptionId,
                    resourceGroup: args.groupName,
                    connectionString: blobConnection.connectionString,
                    container: null
                }));
                await config.save();
            }
        }

        console.log(`${config.getPath()} created.`);

        if (args.secret) {
            console.log(`\nThe secret used to decrypt ${config.getPath()} is:`);
            console.log(chalk.default.magentaBright(args.secret));

            console.log(chalk.default.yellowBright(`\nNOTE: This secret is not recoverable and you should save it in a safe place according to best security practices.`));
            console.log(chalk.default.yellowBright(`      Copy this secret and use it to open the ${config.getPath()} the first time.`));
            await config.save(args.secret);
        }

        if (azBot) {
            // update local safe settings
            await updateLocalSafeSettings(azBot);

            // publish local bot code to web service
            await publishBot(azBot);

            // show emulator url with secret if exist
            let fullPath = path.resolve(process.cwd(), config.getPath());
            let productionEndpoint = config.findServiceByNameOrId('production');
            let botFileUrl = `bfemulator://bot.open?path=${encodeURIComponent(fullPath)}`;
            if (args.secret) {
                botFileUrl += `&secret=${encodeURIComponent(args.secret)}`;
            }
            if (productionEndpoint) {
                botFileUrl += `&id=${productionEndpoint.id}`;
            }
            console.log('To open this bot file in emulator:');
            console.log(chalk.default.cyanBright(botFileUrl));

            // auto launch emulator with url so it can memorize the secret so you don't need to remember it.  <whew!>
            process.env.ELECTRON_NO_ATTACH_CONSOLE = "true";
            opn(botFileUrl, { wait: false });
        }

        console.log(`Done.`);
    } catch (error) {
        if (error.message) {

            let lines = error.message.split('\n');
            let message = '';
            for (let line of lines) {
                // trim to copyright symbol, help from inner process command line args is inappropriate
                if (line.indexOf('©') > 0)
                    break;
                message += line;
            }
            throw new Error(message);
        }
        throw new Error(error);
    }
}