diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index d1861d70f..3d435690b 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -15,6 +15,7 @@ const { localConfig, globalConfig } = require("../config"); const { questionsCreateFunction, questionsCreateFunctionSelectTemplate, + questionsCreateSite, questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection, @@ -25,11 +26,13 @@ const { } = require("../questions"); const { cliConfig, success, log, hint, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); +const { sitesListTemplates } = require("./sites"); const { sdkForConsole } = require("../sdks"); const initResources = async () => { const actions = { function: initFunction, + site: initSite, collection: initCollection, bucket: initBucket, team: initTeam, @@ -318,6 +321,188 @@ const initFunction = async () => { log("Next you can use 'appwrite run function' to develop a function locally. To deploy the function, use 'appwrite push function'"); } +const initSite = async () => { + process.chdir(localConfig.configDirectoryPath) + + const answers = await inquirer.prompt(questionsCreateSite); + const siteFolder = path.join(process.cwd(), 'sites'); + + if (!fs.existsSync(siteFolder)) { + fs.mkdirSync(siteFolder, { + recursive: true + }); + } + + const siteId = answers.id === 'unique()' ? ID.unique() : answers.id; + const siteName = answers.name; + const siteDir = path.join(siteFolder, siteName); + const templatesDir = path.join(siteFolder, `${siteId}-templates`); + + if (fs.existsSync(siteDir)) { + throw new Error(`( ${siteName} ) already exists in the current directory. Please choose another name.`); + } + + let templateDetails; + try { + const response = await sitesListTemplates({ + frameworks: [answers.framework.key], + useCases: ['starter'], + limit: 1, + parseOutput: false + }); + if (response.total == 0) { + throw new Error(`No starter template found for framework ${answers.framework.key}`); + } + templateDetails = response.templates[0]; + } catch (error) { + throw new Error(`Failed to fetch template for framework ${answers.framework.key}: ${error.message}`); + } + + fs.mkdirSync(siteDir, "777"); + fs.mkdirSync(templatesDir, "777"); + const repo = `https://github.com/${templateDetails.providerOwner}/${templateDetails.providerRepositoryId}`; + let selected = { template: templateDetails.frameworks[0].providerRootDirectory }; + + let gitCloneCommands = ''; + + const sparse = selected.template.startsWith('./') ? selected.template.substring(2) : selected.template; + + log('Fetching site code ...'); + + if(selected.template === './') { + gitCloneCommands = ` + mkdir -p . + cd . + git init + git remote add origin ${repo} + git config --global init.defaultBranch main + git fetch --depth=1 origin refs/tags/$(git ls-remote --tags origin "${templateDetails.providerVersion}" | tail -n 1 | awk -F '/' '{print $3}') + git checkout FETCH_HEAD + `.trim(); + } else { + gitCloneCommands = ` + mkdir -p . + cd . + git init + git remote add origin ${repo} + git config --global init.defaultBranch main + git config core.sparseCheckout true + echo "${sparse}" >> .git/info/sparse-checkout + git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' + git config remote.origin.tagopt --no-tags + git fetch --depth=1 origin refs/tags/$(git ls-remote --tags origin "${templateDetails.providerVersion}" | tail -n 1 | awk -F '/' '{print $3}') + git checkout FETCH_HEAD + `.trim(); + } + + /* Force use CMD as powershell does not support && */ + if (process.platform === 'win32') { + gitCloneCommands = 'cmd /c "' + gitCloneCommands + '"'; + } + + /* Execute the child process but do not print any std output */ + try { + childProcess.execSync(gitCloneCommands, { stdio: 'pipe', cwd: templatesDir }); + } catch (error) { + /* Specialised errors with recommended actions to take */ + if (error.message.includes('error: unknown option')) { + throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) + } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { + throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) + } else { + throw error; + } + } + + fs.rmSync(path.join(templatesDir, ".git"), { recursive: true }); + + const copyRecursiveSync = (src, dest) => { + let exists = fs.existsSync(src); + let stats = exists && fs.statSync(src); + let isDirectory = exists && stats.isDirectory(); + if (isDirectory) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + + fs.readdirSync(src).forEach(function (childItemName) { + copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + fs.copyFileSync(src, dest); + } + }; + copyRecursiveSync(selected.template === './' ? templatesDir : path.join(templatesDir, selected.template), siteDir); + + fs.rmSync(templatesDir, { recursive: true, force: true }); + + const readmePath = path.join(process.cwd(), 'sites', siteName, 'README.md'); + const readmeFile = fs.readFileSync(readmePath).toString(); + const newReadmeFile = readmeFile.split('\n'); + newReadmeFile[0] = `# ${answers.key}`; + newReadmeFile.splice(1, 2); + fs.writeFileSync(readmePath, newReadmeFile.join('\n')); + + let vars = (templateDetails.variables ?? []).map(variable => { + let value = variable.value; + const replacements = { + '{apiEndpoint}': globalConfig.getEndpoint(), + '{projectId}': localConfig.getProject().projectId, + '{projectName}': localConfig.getProject().projectName, + }; + + for (const placeholder in replacements) { + if (value?.includes(placeholder)) { + value = value.replace(placeholder, replacements[placeholder]); + } + } + + return { + key: variable.name, + value: value + }; + }); + + let data = { + $id: siteId, + name: answers.name, + framework: answers.framework.key, + adapter: templateDetails.frameworks[0].adapter || '', + buildRuntime: templateDetails.frameworks[0].buildRuntime || '', + installCommand: templateDetails.frameworks[0].installCommand || '', + buildCommand: templateDetails.frameworks[0].buildCommand || '', + outputDirectory: templateDetails.frameworks[0].outputDirectory || '', + fallbackFile: templateDetails.frameworks[0].fallbackFile || '', + specification: answers.specification, + enabled: true, + timeout: 30, + logging: true, + ignore: answers.framework.ignore || null, + path: `sites/${siteName}`, + vars: vars + }; + + if (!data.buildRuntime) { + log(`Build runtime for this framework not found. You will be asked to configure build runtime when you first push the site.`); + } + + if (!data.installCommand) { + log(`Installation command for this framework not found. You will be asked to configure the install command when you first push the site.`); + } + + if (!data.buildCommand) { + log(`Build command for this framework not found. You will be asked to configure the build command when you first push the site.`); + } + + if (!data.outputDirectory) { + log(`Output directory for this framework not found. You will be asked to configure the output directory when you first push the site.`); + } + + localConfig.addSite(data); + success("Initializing site"); + log("Next you can use 'appwrite push site' to deploy the changes."); +}; + const init = new Command("init") .description(commandDescriptions['init']) .action(actionRunner(initResources)); @@ -336,6 +521,12 @@ init .description("Init a new {{ spec.title|caseUcfirst }} function") .action(actionRunner(initFunction)); +init + .command("site") + .alias("sites") + .description("Init a new {{ spec.title|caseUcfirst }} site") + .action(actionRunner(initSite)); + init .command("bucket") .alias("buckets") diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index f9b6bf941..89d0beadc 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -7,17 +7,19 @@ const { messagingListTopics } = require("./messaging"); const { teamsList } = require("./teams"); const { projectsGet } = require("./projects"); const { functionsList, functionsGetDeploymentDownload, functionsListDeployments } = require("./functions"); +const { sitesList, sitesGetDeploymentDownload, sitesListDeployments } = require("./sites"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); const { storageListBuckets } = require("./storage"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); -const { questionsPullCollection, questionsPullFunctions, questionsPullFunctionsCode, questionsPullResources } = require("../questions"); +const { questionsPullCollection, questionsPullFunctions, questionsPullFunctionsCode, questionsPullSites, questionsPullSitesCode, questionsPullResources } = require("../questions"); const { cliConfig, success, log, warn, actionRunner, commandDescriptions } = require("../parser"); const pullResources = async () => { const actions = { settings: pullSettings, functions: pullFunctions, + sites: pullSites, collections: pullCollection, buckets: pullBucket, teams: pullTeam, @@ -169,6 +171,119 @@ const pullFunctions = async ({ code, withVariables }) => { success(`Successfully pulled ${chalk.bold(total)} functions.`); } +const pullSites = async ({ code, withVariables }) => { + process.chdir(localConfig.configDirectoryPath) + + log("Fetching sites ..."); + let total = 0; + + const fetchResponse = await sitesList({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["sites"].length <= 0) { + log("No sites found."); + success(`Successfully pulled ${chalk.bold(total)} sites.`); + return; + } + + const sites = cliConfig.all + ? (await paginate(sitesList, { parseOutput: false }, 100, 'sites')).sites + : (await inquirer.prompt(questionsPullSites)).sites; + + let allowCodePull = cliConfig.force === true ? true : null; + + for (let site of sites) { + total++; + log(`Pulling site ${chalk.bold(site['name'])} ...`); + + const localSite = localConfig.getSite(site.$id); + + site['path'] = localSite['path']; + if (!localSite['path']) { + site['path'] = `sites/${site.name}`; + } + const holdingVars = site['vars']; + // We don't save var in to the config + delete site['vars']; + localConfig.addSite(site); + + if (!fs.existsSync(site['path'])) { + fs.mkdirSync(site['path'], { recursive: true }); + } + + if (code === false) { + warn("Source code download skipped."); + continue; + } + + if (allowCodePull === null) { + const codeAnswer = await inquirer.prompt(questionsPullSitesCode); + allowCodePull = codeAnswer.override; + } + + if (!allowCodePull) { + continue; + } + + let deploymentId = null; + + try { + const fetchResponse = await sitesListDeployments({ + siteId: site['$id'], + queries: [ + JSON.stringify({ method: 'limit', values: [1] }), + JSON.stringify({ method: 'orderDesc', values: ['$id'] }) + ], + parseOutput: false + }); + + if (fetchResponse['total'] > 0) { + deploymentId = fetchResponse['deployments'][0]['$id']; + } + + } catch { + } + + if (deploymentId === null) { + log("Source code download skipped because site doesn't have any available deployment"); + continue; + } + + log("Pulling latest deployment code ..."); + + const compressedFileName = `${site['$id']}-${+new Date()}.tar.gz` + await sitesGetDeploymentDownload({ + siteId: site['$id'], + deploymentId, + destination: compressedFileName, + overrideForCli: true, + parseOutput: false + }); + + tar.extract({ + sync: true, + cwd: site['path'], + file: compressedFileName, + strict: false, + }); + + fs.rmSync(compressedFileName); + + if (withVariables) { + const envFileLocation = `${site['path']}/.env` + try { + fs.rmSync(envFileLocation); + } catch { + } + + fs.writeFileSync(envFileLocation, holdingVars.map(r => `${r.key}=${r.value}\n`).join('')) + } + } + + success(`Successfully pulled ${chalk.bold(total)} sites.`); +} + const pullCollection = async () => { log("Fetching collections ..."); let total = 0; @@ -321,6 +436,14 @@ pull .option("--with-variables", `Pull function variables. ${chalk.red('recommend for testing purposes only')}`) .action(actionRunner(pullFunctions)) +pull + .command("site") + .alias("sites") + .description("Pull your {{ spec.title|caseUcfirst }} site") + .option("--no-code", "Don't pull the site's code") + .option("--with-variables", `Pull site variables. ${chalk.red('recommend for testing purposes only')}`) + .action(actionRunner(pullSites)) + pull .command("collection") .alias("collections") diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index fd018fc10..f6c0d2d49 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -3,15 +3,16 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const ID = require("../id"); -const { localConfig, globalConfig, KeysAttributes, KeysFunction, whitelistKeys, KeysTopics, KeysStorage, KeysTeams, KeysCollection } = require("../config"); +const { localConfig, globalConfig, KeysAttributes, KeysFunction, KeysSite, whitelistKeys, KeysTopics, KeysStorage, KeysTeams, KeysCollection } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionPushChanges, questionPushChangesConfirmation, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsPushSites, questionsGetEntrypoint, questionsPushCollections, questionPushChanges, questionPushChangesConfirmation, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); const { cliConfig, actionRunner, success, warn, log, hint, error, commandDescriptions, drawTable } = require("../parser"); -const { proxyCreateFunctionRule, proxyListRules } = require('./proxy'); +const { proxyCreateFunctionRule, proxyCreateSiteRule, proxyListRules } = require('./proxy'); const { consoleVariables } = require('./console'); const { sdkForConsole } = require('../sdks') const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); +const { sitesGet, sitesCreate, sitesUpdate, sitesCreateDeployment, sitesGetDeployment, sitesCreateVariable } = require('./sites'); const { databasesGet, databasesCreate, @@ -899,6 +900,7 @@ const pushResources = async () => { const actions = { settings: pushSettings, functions: pushFunction, + sites: pushSite, collections: pushCollection, buckets: pushBucket, teams: pushTeam, @@ -1009,6 +1011,303 @@ const pushSettings = async () => { } } +const pushSite = async({ siteId, async, code } = { returnOnZero: false }) => { + process.chdir(localConfig.configDirectoryPath) + + const siteIds = []; + + if(siteId) { + siteIds.push(siteId); + } else if (cliConfig.all) { + checkDeployConditions(localConfig); + const sites = localConfig.getSites(); + siteIds.push(...sites.map((site) => { + return site.$id; + })); + } + + if (siteIds.length <= 0) { + const answers = await inquirer.prompt(questionsPushSites[0]); + if (answers.sites) { + siteIds.push(...answers.sites); + } + } + + if (siteIds.length === 0) { + log("No sites found."); + hint("Use 'appwrite pull sites' to synchronize existing one, or use 'appwrite init site' to create a new one."); + return; + } + + let sites = siteIds.map((id) => { + const sites = localConfig.getSites(); + const site = sites.find((s) => s.$id === id); + + if (!site) { + throw new Error("Site '" + id + "' not found.") + } + + return site; + }); + + log('Validating sites ...'); + // Validation is done BEFORE pushing so the deployment process can be run in async with progress update + for (let site of sites) { + + if (!site.buildCommand) { + log(`Site ${site.name} is missing build command.`); + const answers = await inquirer.prompt(questionsGetBuildCommand) + site.buildCommand = answers.buildCommand; + localConfig.addSite(site); + } + } + + if (!(await approveChanges(sites, sitesGet, KeysSite, 'siteId', 'sites', ['vars']))) { + return; + } + + log('Pushing sites ...'); + + Spinner.start(false); + let successfullyPushed = 0; + let successfullyDeployed = 0; + const failedDeployments = []; + const errors = []; + + await Promise.all(sites.map(async (site) => { + let response = {}; + + const ignore = site.ignore ? 'appwrite.json' : '.gitignore'; + let siteExists = false; + let deploymentCreated = false; + + const updaterRow = new Spinner({ status: '', resource: site.name, id: site['$id'], end: `Ignoring using: ${ignore}` }); + + updaterRow.update({ status: 'Getting' }).startSpinner(SPINNER_DOTS); + + try { + response = await sitesGet({ + siteId: site['$id'], + parseOutput: false, + }); + siteExists = true; + if (response.framework !== site.framework) { + updaterRow.fail({ errorMessage: `Framework mismatch! (local=${site.framework},remote=${response.framework}) Please delete remote site or update your appwrite.json` }) + return; + } + + updaterRow.update({ status: 'Updating' }).replaceSpinner(SPINNER_ARC); + + response = await sitesUpdate({ + siteId: site['$id'], + name: site.name, + framework: site.framework, + buildRuntime: site.buildRuntime, + specification: site.specification, + timeout: site.timeout, + enabled: site.enabled, + logging: site.logging, + adapter: site.adapter, + buildCommand: site.buildCommand, + installCommand: site.installCommand, + outputDirectory: site.outputDirectory, + fallbackFile: site.fallbackFile, + vars: JSON.stringify(response.vars), + parseOutput: false + }); + } catch (e) { + + if (Number(e.code) === 404) { + siteExists = false; + } else { + errors.push(e); + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); + return; + } + } + + if (!siteExists) { + updaterRow.update({ status: 'Creating' }).replaceSpinner(SPINNER_DOTS); + + try { + response = await sitesCreate({ + siteId: site.$id, + name: site.name, + framework: site.framework, + specification: site.specification, + buildRuntime: site.buildRuntime, + buildCommand: site.buildCommand, + installCommand: site.installCommand, + outputDirectory: site.outputDirectory, + fallbackFile: site.fallbackFile, + adapter: site.adapter, + timeout: site.timeout, + enabled: site.enabled, + logging: site.logging, + vars: JSON.stringify(site.vars), + parseOutput: false + }); + + let domain = ''; + try { + const variables = await consoleVariables({ parseOutput: false, sdk: await sdkForConsole() }); + domain = ID.unique() + '.' + variables['_APP_DOMAIN_SITES']; + } catch (error) { + console.error('Error fetching console variables.'); + throw error; + } + + try { + const rule = await proxyCreateSiteRule( + { + domain: domain, + siteId: site.$id + } + ); + } catch (error) { + console.error('Error creating site rule.'); + throw error; + } + + updaterRow.update({ status: 'Created' }); + } catch (e) { + errors.push(e) + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); + return; + } + } + + updaterRow.update({ status: 'Creating variables' }).replaceSpinner(SPINNER_ARC); + + await Promise.all((site['vars'] ?? []).map(async variable => { + await sitesCreateVariable({ + siteId: site['$id'], + key: variable['key'], + value: variable['value'], + parseOutput: false + }); + })); + + if (code === false) { + successfullyPushed++; + successfullyDeployed++; + updaterRow.update({ status: 'Pushed' }); + updaterRow.stopSpinner(); + return; + } + + try { + updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); + response = await sitesCreateDeployment({ + siteId: site['$id'], + buildCommand: site.buildCommand, + installCommand: site.installCommand, + outputDirectory: site.outputDirectory, + fallbackFile: site.fallbackFile, + code: site.path, + activate: true, + parseOutput: false + }) + + updaterRow.update({ status: 'Pushed' }); + deploymentCreated = true; + successfullyPushed++; + } catch (e) { + errors.push(e); + + switch (e.code) { + case 'ENOENT': + updaterRow.fail({ errorMessage: 'Not found in the current directory. Skipping...' }) + break; + default: + updaterRow.fail({ errorMessage: e.message ?? 'An unknown error occurred. Please try again.' }) + } + } + + if (deploymentCreated && !async) { + try { + const deploymentId = response['$id']; + updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) + let pollChecks = 0; + + while (true) { + response = await sitesGetDeployment({ + siteId: site['$id'], + deploymentId: deploymentId, + parseOutput: false + }); + + const status = response['status']; + if (status === 'ready') { + successfullyDeployed++; + + let url = ''; + const res = await proxyListRules({ + parseOutput: false, + queries: [ + JSON.stringify({ method: 'limit', values: [1] }), + JSON.stringify({ method: 'equal', "attribute": "deploymentResourceType", "values": ["site"] }), + JSON.stringify({ method: 'equal', "attribute": "deploymentResourceId", "values": [site['$id']] }), + JSON.stringify({ method: 'equal', "attribute": "trigger", "values": ["manual"] }), + ], + }); + + if (Number(res.total) === 1) { + url = res.rules[0].domain; + } + + updaterRow.update({ status: 'Deployed', end: url }); + + break; + } else if (status === 'failed') { + failedDeployments.push({ name: site['name'], $id: site['$id'], deployment: response['$id'] }); + updaterRow.fail({ errorMessage: `Failed to deploy` }); + + break; + } else { + updaterRow.update({ status: 'Deploying', end: `Current status: ${status}` }) + } + + pollChecks++; + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE * 1.5)); + } + } catch (e) { + errors.push(e); + updaterRow.fail({ errorMessage: e.message ?? 'Unknown error occurred. Please try again' }) + } + } + + updaterRow.stopSpinner(); + })); + + Spinner.stop(); + + failedDeployments.forEach((failed) => { + const { name, deployment, $id } = failed; + const failUrl = `${globalConfig.getEndpoint().slice(0, -3)}/console/project-${localConfig.getProject().projectId}/sites/site-${$id}/deployments/deployment-${deployment}`; + + error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); + }); + + if (!async) { + if (successfullyPushed === 0) { + error('No sites were pushed.'); + } else if (successfullyDeployed !== successfullyPushed) { + warn(`Successfully pushed ${successfullyDeployed} of ${successfullyPushed} sites`) + } else { + success(`Successfully pushed ${successfullyPushed} sites.`); + } + } else { + success(`Successfully pushed ${successfullyPushed} sites.`); + } + + if (cliConfig.verbose) { + errors.forEach(e => { + console.error(e); + }) + } +} + const pushFunction = async ({ functionId, async, code, withVariables } = { returnOnZero: false }) => { process.chdir(localConfig.configDirectoryPath) @@ -1723,6 +2022,15 @@ push .option("--with-variables", `Push function variables.`) .action(actionRunner(pushFunction)); +push + .command("site") + .alias("sites") + .description("Push sites in the current directory.") + .option(`-f, --site-id `, `ID of site to run`) + .option(`-A, --async`, `Don't wait for sites deployments status`) + .option("--no-code", "Don't push the site's code") + .action(actionRunner(pushSite)); + push .command("collection") .alias("collections") diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index d39677a6d..daeff2342 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -5,7 +5,7 @@ const process = require("process"); const JSONbig = require("json-bigint")({ storeAsString: false }); const KeysVars = new Set(["key", "value"]); -const KeysSite = new Set(["path", "$id", "name", "enabled", "logging", "timeout", "framework", "buildRuntime", "adapter", "installCommand", "buildCommand", "outputDirectory", "fallbackFile", "specification"]); +const KeysSite = new Set(["path", "$id", "name", "enabled", "logging", "timeout", "framework", "buildRuntime", "adapter", "installCommand", "buildCommand", "outputDirectory", "fallbackFile", "specification", "vars"]); const KeysFunction = new Set(["path", "$id", "execute", "name", "enabled", "logging", "runtime", "specification", "scopes", "events", "schedule", "timeout", "entrypoint", "commands", "vars"]); const KeysDatabase = new Set(["$id", "name", "enabled"]); const KeysCollection = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "attributes", "indexes"]); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 220d06426..f9f923984 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -13,6 +13,7 @@ const { isPortTaken } = require('./utils'); const { databasesList } = require('./commands/databases'); const { checkDeployConditions } = require('./utils'); const JSONbig = require("json-bigint")({ storeAsString: false }); +const { sitesListFrameworks, sitesListSpecifications, sitesList } = require('./commands/sites'); const whenOverride = (answers) => answers.override === undefined ? true : answers.override; @@ -269,6 +270,36 @@ const questionsPullFunctionsCode = [ }, ]; +const questionsPullSites = [ + { + type: "checkbox", + name: "sites", + message: "Which sites would you like to pull?", + validate: (value) => validateRequired('site', value), + choices: async () => { + const { sites } = await paginate(sitesList, { parseOutput: false }, 100, 'sites'); + + if (sites.length === 0) { + throw "We couldn't find any sites in your {{ spec.title|caseUcfirst }} project"; + } + return sites.map(site => { + return { + name: `${site.name} (${site.$id})`, + value: { ...site } + } + }); + } + } +]; + +const questionsPullSitesCode = [ + { + type: "confirm", + name: "override", + message: "Do you want to pull source code of the latest deployment?" + }, +]; + const questionsCreateFunction = [ { type: "input", @@ -635,6 +666,27 @@ const questionsInitResources = [ } ]; +const questionsPushSites = [ + { + type: "checkbox", + name: "sites", + message: "Which sites would you like to push?", + validate: (value) => validateRequired('site', value), + when: () => localConfig.getSites().length > 0, + choices: () => { + let sites = localConfig.getSites(); + checkDeployConditions(localConfig) + let choices = sites.map((site, idx) => { + return { + name: `${site.name} (${site.$id})`, + value: site.$id + } + }) + return choices; + } + }, +]; + const questionsPushFunctions = [ { type: "checkbox", @@ -842,6 +894,58 @@ const questionsRunFunctions = [ } ]; +const questionsCreateSite = [ + { + type: "input", + name: "name", + message: "What would you like to name your site?", + default: "My Awesome Site" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your site?", + default: "unique()" + }, + { + type: "list", + name: "framework", + message: "What framework would you like to use?", + choices: async () => { + let response = await sitesListFrameworks({ + parseOutput: false + }); + let frameworks = response["frameworks"]; + let choices = frameworks.map((framework) => { + return { + name: `${framework.name} (${framework.key})`, + value: framework, + } + }); + return choices; + }, + }, + { + type: "list", + name: "specification", + message: "What specification would you like to use?", + choices: async () => { + let response = await sitesListSpecifications({ + parseOutput: false + }); + let specifications = response["specifications"]; + let choices = specifications.map((spec) => { + return { + name: `${spec.cpus} CPU, ${spec.memory}MB RAM`, + value: spec.slug, + disabled: spec.enabled === false ? 'Upgrade to use' : false + } + }); + return choices; + }, + } +]; + module.exports = { questionsInitProject, questionsInitProjectAutopull, @@ -852,12 +956,15 @@ module.exports = { questionsCreateMessagingTopic, questionsPullFunctions, questionsPullFunctionsCode, + questionsPullSites, + questionsPullSitesCode, questionsLogin, questionsPullResources, questionsLogout, questionsPullCollection, questionsPushResources, questionsPushFunctions, + questionsPushSites, questionsPushCollections, questionsPushBuckets, questionsPushMessagingTopics, @@ -870,5 +977,6 @@ module.exports = { questionsInitResources, questionsCreateTeam, questionPushChanges, - questionPushChangesConfirmation + questionPushChangesConfirmation, + questionsCreateSite };