From c99bde3d4a6d7beebaa029da12428d44f8e40569 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 8 Jul 2024 20:59:45 +0200 Subject: [PATCH] fix: qa --- README.md | 4 +- index.js | 3 +- install.ps1 | 4 +- install.sh | 2 +- lib/client.js | 4 +- lib/commands/generic.js | 22 ++++- lib/commands/init.js | 41 +++++--- lib/commands/pull.js | 190 ++++++++++++++++++++++++------------- lib/commands/push.js | 205 +++++++++++++--------------------------- lib/commands/run.js | 72 +++++++------- lib/config.js | 72 ++++++++++---- lib/emulation/docker.js | 100 +++++++++++++------- lib/emulation/utils.js | 11 ++- lib/parser.js | 22 +++-- lib/questions.js | 68 +++++++++---- lib/sdks.js | 38 -------- package.json | 2 +- scoop/appwrite.json | 6 +- 18 files changed, 465 insertions(+), 401 deletions(-) diff --git a/README.md b/README.md index eb351d6..0841ddc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Once the installation is complete, you can verify the install using ```sh $ appwrite -v -6.0.0-rc.1 +6.0.0-rc.2 ``` ### Install using prebuilt binaries @@ -60,7 +60,7 @@ $ scoop install https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/sc Once the installation completes, you can verify your install using ``` $ appwrite -v -6.0.0-rc.1 +6.0.0-rc.2 ``` ## Getting Started diff --git a/index.js b/index.js index a1db573..58b9dcd 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,7 @@ const { login, logout, whoami, migrate, register } = require("./lib/commands/gen const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { run } = require("./lib/commands/run"); -const { push } = require("./lib/commands/push"); +const { push, deploy } = require("./lib/commands/push"); const { account } = require("./lib/commands/account"); const { avatars } = require("./lib/commands/avatars"); const { assistant } = require("./lib/commands/assistant"); @@ -77,6 +77,7 @@ program .addCommand(init) .addCommand(pull) .addCommand(push) + .addCommand(deploy) .addCommand(run) .addCommand(logout) .addCommand(account) diff --git a/install.ps1 b/install.ps1 index b33caae..f6a9445 100644 --- a/install.ps1 +++ b/install.ps1 @@ -13,8 +13,8 @@ # You can use "View source" of this page to see the full script. # REPO -$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.1/appwrite-cli-win-x64.exe" -$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.1/appwrite-cli-win-arm64.exe" +$GITHUB_x64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.2/appwrite-cli-win-x64.exe" +$GITHUB_arm64_URL = "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.2/appwrite-cli-win-arm64.exe" $APPWRITE_BINARY_NAME = "appwrite.exe" diff --git a/install.sh b/install.sh index 668926b..7d41d28 100644 --- a/install.sh +++ b/install.sh @@ -97,7 +97,7 @@ printSuccess() { downloadBinary() { echo "[2/4] Downloading executable for $OS ($ARCH) ..." - GITHUB_LATEST_VERSION="6.0.0-rc.1" + GITHUB_LATEST_VERSION="6.0.0-rc.2" GITHUB_FILE="appwrite-cli-${OS}-${ARCH}" GITHUB_URL="https://github.com/$GITHUB_REPOSITORY_NAME/releases/download/$GITHUB_LATEST_VERSION/$GITHUB_FILE" diff --git a/lib/client.js b/lib/client.js index 19d06aa..050c65e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -16,8 +16,8 @@ class Client { 'x-sdk-name': 'Command Line', 'x-sdk-platform': 'console', 'x-sdk-language': 'cli', - 'x-sdk-version': '6.0.0-rc.1', - 'user-agent' : `AppwriteCLI/6.0.0-rc.1 (${os.type()} ${os.version()}; ${os.arch()})`, + 'x-sdk-version': '6.0.0-rc.2', + 'user-agent' : `AppwriteCLI/6.0.0-rc.2 (${os.type()} ${os.version()}; ${os.arch()})`, 'X-Appwrite-Response-Format' : '1.5.0', }; } diff --git a/lib/commands/generic.js b/lib/commands/generic.js index c6b1a4c..bc913fe 100644 --- a/lib/commands/generic.js +++ b/lib/commands/generic.js @@ -3,7 +3,7 @@ const { Command } = require("commander"); const Client = require("../client"); const { sdkForConsole } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable, cliConfig } = require("../parser"); +const { actionRunner, success, parseBool, commandDescriptions, error, parse, hint, log, drawTable, cliConfig } = require("../parser"); const ID = require("../id"); const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); @@ -14,8 +14,20 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { const oldCurrent = globalConfig.getCurrentSession(); let configEndpoint = endpoint ?? DEFAULT_ENDPOINT; + if(globalConfig.getCurrentSession() !== '') { + log('You are currently signed in as ' + globalConfig.getEmail()); + + if(globalConfig.getSessions().length === 1) { + hint('You can sign in and manage multiple accounts with Appwrite CLI'); + } + } + const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); + if(!answers.method) { + answers.method = 'login'; + } + if (answers.method === 'select') { const accountId = answers.accountId; @@ -87,15 +99,15 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { } } - success("Signed in as user with ID: " + account.$id); - log("Next you can create or link to your project using 'appwrite init project'"); + success("Successfully signed in as " + account.email); + hint("Next you can create or link to your project using 'appwrite init project'"); }; const whoami = new Command("whoami") .description(commandDescriptions['whoami']) .action(actionRunner(async () => { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - error("No user is signed in. To sign in, run: appwrite login "); + error("No user is signed in. To sign in, run 'appwrite login'"); return; } @@ -109,7 +121,7 @@ const whoami = new Command("whoami") parseOutput: false }); } catch (error) { - error("No user is signed in. To sign in, run: appwrite login"); + error("No user is signed in. To sign in, run 'appwrite login'"); return; } diff --git a/lib/commands/init.js b/lib/commands/init.js index ebd74e8..9cbe03b 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -9,6 +9,7 @@ const { storageCreateBucket } = require("./storage"); const { messagingCreateTopic } = require("./messaging"); const { functionsCreate } = require("./functions"); const { databasesCreateCollection } = require("./databases"); +const { pullResources } = require("./pull"); const ID = require("../id"); const { localConfig, globalConfig } = require("../config"); const { @@ -18,10 +19,11 @@ const { questionsCreateMessagingTopic, questionsCreateCollection, questionsInitProject, + questionsInitProjectAutopull, questionsInitResources, questionsCreateTeam } = require("../questions"); -const { success, log, error, actionRunner, commandDescriptions } = require("../parser"); +const { cliConfig, success, log, hint, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { sdkForConsole } = require("../sdks"); @@ -56,7 +58,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { sdk: client }); } catch (e) { - error('Error Session not found. Please run `appwrite login` to create a session'); + error("Error Session not found. Please run 'appwrite login' to create a session"); process.exit(1); } @@ -104,11 +106,17 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { success(`Project successfully ${answers.start === 'existing' ? 'linked' : 'created'}. Details are now stored in appwrite.json file.`); - log("Next you can use 'appwrite init' to create resources in your project, or use 'appwrite pull' and 'appwite push' to synchronize your project.") - if(answers.start === 'existing') { - log("Since you connected to an existing project, we highly recommend to run 'appwrite pull all' to synchronize all of your existing resources."); + answers = await inquirer.prompt(questionsInitProjectAutopull); + if(answers.autopull) { + cliConfig.all = true; + await pullResources(); + } else { + log("You can run 'appwrite pull all' to synchronize all of your existing resources."); + } } + + hint("Next you can use 'appwrite init' to create resources in your project, or use 'appwrite pull' and 'appwrite push' to synchronize your project.") } const initBucket = async () => { @@ -210,22 +218,24 @@ const initFunction = async () => { log(`Installation command for this runtime not found. You will be asked to configure the install command when you first push the function.`); } - fs.mkdirSync(functionDir, "777"); fs.mkdirSync(templatesDir, "777"); const repo = "https://github.com/appwrite/templates"; const api = `https://api.github.com/repos/appwrite/templates/contents/${answers.runtime.name}` - const templates = ['starter']; let selected = undefined; - try { - const res = await fetch(api); - templates.push(...(await res.json()).map((template) => template.name)); - - selected = await inquirer.prompt(questionsCreateFunctionSelectTemplate(templates)) - } catch { - // Not a problem will go with directory pulling - log('Loading templates...'); + if(answers.template === 'starter') { + selected = { template: 'starter' }; + } else { + try { + const res = await fetch(api); + const templates = []; + templates.push(...(await res.json()).map((template) => template.name)); + selected = await inquirer.prompt(questionsCreateFunctionSelectTemplate(templates)); + } catch { + // Not a problem will go with directory pulling + log('Loading templates...'); + } } const sparse = (selected ? `${answers.runtime.name}/${selected.template}` : answers.runtime.name).toLowerCase(); @@ -257,6 +267,7 @@ const initFunction = async () => { fs.rmSync(path.join(templatesDir, ".git"), { recursive: true }); if (!selected) { + const templates = []; templates.push(...fs.readdirSync(runtimeDir, { withFileTypes: true }) .filter(item => item.isDirectory() && item.name !== 'starter') .map(dirent => dirent.name)); diff --git a/lib/commands/pull.js b/lib/commands/pull.js index cea0331..5063e51 100644 --- a/lib/commands/pull.js +++ b/lib/commands/pull.js @@ -1,4 +1,5 @@ const fs = require("fs"); +const chalk = require('chalk'); const tar = require("tar"); const { Command } = require("commander"); const inquirer = require("inquirer"); @@ -11,11 +12,11 @@ const { storageListBuckets } = require("./storage"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); const { questionsPullCollection, questionsPullFunctions, questionsPullResources } = require("../questions"); -const { cliConfig, success, log, actionRunner, commandDescriptions } = require("../parser"); +const { cliConfig, success, log, warn, actionRunner, commandDescriptions } = require("../parser"); const pullResources = async () => { const actions = { - project: pullProject, + settings: pullSettings, functions: pullFunctions, collections: pullCollection, buckets: pullBucket, @@ -25,6 +26,7 @@ const pullResources = async () => { if (cliConfig.all) { for (let action of Object.values(actions)) { + cliConfig.all = true; await action(); } } else { @@ -37,75 +39,94 @@ const pullResources = async () => { } }; -const pullProject = async () => { +const pullSettings = async () => { + log("Pulling project settings ..."); + try { let response = await projectsGet({ parseOutput: false, projectId: localConfig.getProject().projectId - - }) + }); localConfig.setProject(response.$id, response.name, response); - success(); + success(`Successfully pulled ${chalk.bold('all')} project settings.`); } catch (e) { throw e; } } const pullFunctions = async () => { - const localFunctions = localConfig.getFunctions(); + log("Fetching functions ..."); + let total = 0; + + const fetchResponse = await functionsList({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["functions"].length <= 0) { + log("No functions found."); + success(`Successfully pulled ${chalk.bold(total)} functions.`); + return; + } const functions = cliConfig.all ? (await paginate(functionsList, { parseOutput: false }, 100, 'functions')).functions : (await inquirer.prompt(questionsPullFunctions)).functions; - log(`Pulling ${functions.length} functions`); - for (let func of functions) { - const functionExistLocally = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']) !== undefined; + total++; + log(`Pulling function ${chalk.bold(func['name'])} ...`); - if (functionExistLocally) { - localConfig.updateFunction(func['$id'], func); - } else { - func['path'] = `functions/${func['$id']}`; - localConfig.addFunction(func); - localFunctions.push(func); + const localFunction = localConfig.getFunction(func.$id); + if(!localFunction['path']) { + func['path'] = `functions/${func.$id}`; } - const localFunction = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']); + localConfig.addFunction(func); - if (localFunction['deployment'] === '') { - continue + if (!fs.existsSync(func['path'])) { + fs.mkdirSync(func['path'], { recursive: true }); } - const compressedFileName = `${func['$id']}-${+new Date()}.tar.gz` + if(func['deployment']) { + const compressedFileName = `${func['$id']}-${+new Date()}.tar.gz` + await functionsDownloadDeployment({ + functionId: func['$id'], + deploymentId: func['deployment'], + destination: compressedFileName, + overrideForCli: true, + parseOutput: false + }); - await functionsDownloadDeployment({ - functionId: func['$id'], - deploymentId: func['deployment'], - destination: compressedFileName, - overrideForCli: true, - parseOutput: false - }) + tar.extract({ + sync: true, + cwd: func['path'], + file: compressedFileName, + strict: false, + }); - if (!fs.existsSync(localFunction['path'])) { - fs.mkdirSync(localFunction['path'], { recursive: true }); + fs.rmSync(compressedFileName); } - - tar.extract({ - sync: true, - cwd: localFunction['path'], - file: compressedFileName, - strict: false, - }); - - fs.rmSync(compressedFileName); - success(`Pulled ${func['name']} code and settings`) } + + success(`Successfully pulled ${chalk.bold(total)} functions.`); } const pullCollection = async () => { + log("Fetching collections ..."); + let total = 0; + + const fetchResponse = await databasesList({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["databases"].length <= 0) { + log("No collections found."); + success(`Successfully pulled ${chalk.bold(total)} collections.`); + return; + } + let databases = cliConfig.ids; if (databases.length === 0) { @@ -122,61 +143,101 @@ const pullCollection = async () => { parseOutput: false }); + total++; + log(`Pulling all collections from ${chalk.bold(database['name'])} database ...`); + localConfig.addDatabase(database); - const { collections, total } = await paginate(databasesListCollections, { + const { collections } = await paginate(databasesListCollections, { databaseId, parseOutput: false }, 100, 'collections'); - log(`Found ${total} collections`); - - collections.map(async collection => { - log(`Fetching ${collection.name} ...`); + for(const collection of collections) { localConfig.addCollection({ ...collection, '$createdAt': undefined, '$updatedAt': undefined }); - }); + } } - success(); + success(`Successfully pulled ${chalk.bold(total)} collections.`); } const pullBucket = async () => { - const { buckets } = await paginate(storageListBuckets, { parseOutput: false }, 100, 'buckets'); + log("Fetching buckets ..."); + let total = 0; - log(`Found ${buckets.length} buckets`); + const fetchResponse = await storageListBuckets({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["buckets"].length <= 0) { + log("No buckets found."); + success(`Successfully pulled ${chalk.bold(total)} buckets.`); + return; + } - buckets.forEach(bucket => localConfig.addBucket(bucket)); + const { buckets } = await paginate(storageListBuckets, { parseOutput: false }, 100, 'buckets'); - success(); + for(const bucket of buckets) { + total++; + log(`Pulling bucket ${chalk.bold(bucket['name'])} ...`); + localConfig.addBucket(bucket); + } + + success(`Successfully pulled ${chalk.bold(total)} buckets.`); } const pullTeam = async () => { - const { teams } = await paginate(teamsList, { parseOutput: false }, 100, 'teams'); - - log(`Found ${teams.length} teams`); + log("Fetching teams ..."); + let total = 0; - teams.forEach(team => { - const { total, $updatedAt, $createdAt, prefs, ...rest } = team; - localConfig.addTeam(rest); + const fetchResponse = await teamsList({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false }); + if (fetchResponse["teams"].length <= 0) { + log("No teams found."); + success(`Successfully pulled ${chalk.bold(total)} teams.`); + return; + } - success(); + const { teams } = await paginate(teamsList, { parseOutput: false }, 100, 'teams'); + + for(const team of teams) { + total++; + log(`Pulling team ${chalk.bold(team['name'])} ...`); + localConfig.addTeam(team); + } + + success(`Successfully pulled ${chalk.bold(total)} teams.`); } const pullMessagingTopic = async () => { - const { topics } = await paginate(messagingListTopics, { parseOutput: false }, 100, 'topics'); + log("Fetching topics ..."); + let total = 0; + + const fetchResponse = await messagingListTopics({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["topics"].length <= 0) { + log("No topics found."); + success(`Successfully pulled ${chalk.bold(total)} topics.`); + return; + } - log(`Found ${topics.length} topics`); + const { topics } = await paginate(messagingListTopics, { parseOutput: false }, 100, 'topics'); - topics.forEach(topic => { + for(const topic of topics) { + total++; + log(`Pulling topic ${chalk.bold(topic['name'])} ...`); localConfig.addMessagingTopic(topic); - }); + } - success(); + success(`Successfully pulled ${chalk.bold(total)} topics.`); } const pull = new Command("pull") @@ -192,9 +253,9 @@ pull })); pull - .command("project") + .command("settings") .description("Pull your Appwrite project name, services and auth settings") - .action(actionRunner(pullProject)); + .action(actionRunner(pullSettings)); pull .command("function") @@ -228,4 +289,5 @@ pull module.exports = { pull, + pullResources }; diff --git a/lib/commands/push.js b/lib/commands/push.js index 1349b73..949b299 100644 --- a/lib/commands/push.js +++ b/lib/commands/push.js @@ -6,7 +6,7 @@ const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); -const { cliConfig, actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); +const { cliConfig, actionRunner, success, warn, log, error, commandDescriptions, drawTable } = require("../parser"); const { proxyListRules } = require('./proxy'); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -714,7 +714,7 @@ const createAttributes = async (attributes, collection) => { const pushResources = async () => { const actions = { - project: pushProject, + settings: pushSettings, functions: pushFunction, collections: pushCollection, buckets: pushBucket, @@ -736,15 +736,16 @@ const pushResources = async () => { } }; -const pushProject = async () => { +const pushSettings = async () => { try { + log("Pushing project settings ..."); + const projectId = localConfig.getProject().projectId; const projectName = localConfig.getProject().projectName; const settings = localConfig.getProject().projectSettings ?? {}; - log(`Updating project ${projectId}`); - if (projectName) { + log("Applying project name ..."); await projectsUpdate({ projectId, name: projectName, @@ -753,7 +754,7 @@ const pushProject = async () => { } if (settings.services) { - log('Updating service statuses'); + log("Applying service statuses ..."); for (let [service, status] of Object.entries(settings.services)) { await projectsUpdateServiceStatus({ projectId, @@ -766,7 +767,7 @@ const pushProject = async () => { if (settings.auth) { if (settings.auth.security) { - log('Updating auth security settings'); + log("Applying auth security settings ..."); await projectsUpdateAuthDuration({ projectId, duration: settings.auth.security.duration, parseOutput: false }); await projectsUpdateAuthLimit({ projectId, limit: settings.auth.security.limit, parseOutput: false }); await projectsUpdateAuthSessionsLimit({ projectId, limit: settings.auth.security.sessionsLimit, parseOutput: false }); @@ -776,7 +777,7 @@ const pushProject = async () => { } if (settings.auth.methods) { - log('Updating auth login methods'); + log("Applying auth methods statuses ..."); for (let [method, status] of Object.entries(settings.auth.methods)) { await projectsUpdateAuthStatus({ @@ -789,7 +790,7 @@ const pushProject = async () => { } } - success("Project configuration updated."); + success(`Successfully pushed ${chalk.bold('all')} project settings.`); } catch (e) { throw e; } @@ -806,11 +807,9 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero checkDeployConditions(localConfig); const functions = localConfig.getFunctions(); if (functions.length === 0) { - if (returnOnZero) { - log('No functions found, skipping'); - return; - } - throw new Error("No functions found in the current directory. Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); + log("No functions found."); + hint("Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); + return; } functionIds.push(...functions.map((func) => { return func.$id; @@ -833,45 +832,19 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero return func; }); - log('Validating functions'); + log('Validating functions ...'); // Validation is done BEFORE pushing so the deployment process can be run in async with progress update for (let func of functions) { if (!func.entrypoint) { - log(`Function ${func.name} does not have an endpoint`); + log(`Function ${func.name} is missing an entrypoint.`); const answers = await inquirer.prompt(questionsGetEntrypoint) func.entrypoint = answers.entrypoint; - localConfig.updateFunction(func['$id'], func); - } - - if (func.variables) { - func.pushVariables = cliConfig.force; - - try { - const { total } = await functionsListVariables({ - functionId: func['$id'], - queries: [JSON.stringify({ method: 'limit', values: [1] })], - parseOutput: false - }); - - if (total === 0) { - func.pushVariables = true; - } else if (total > 0 && !func.pushVariables) { - log(`The function ${func.name} has remote variables setup`); - const variableAnswers = await inquirer.prompt(questionsPushFunctions[1]) - func.pushVariables = variableAnswers.override.toLowerCase() === "yes"; - } - } catch (e) { - if (e.code != 404) { - throw e.message; - } - } + localConfig.addFunction(func); } } - - log('All functions are validated'); - log('Pushing functions\n'); + log('Pushing functions ...'); Spinner.start(false); let successfullyPushed = 0; @@ -934,7 +907,7 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero try { response = await functionsCreate({ - functionId: func.$id || 'unique()', + functionId: func.$id, name: func.name, runtime: func.runtime, execute: func.execute, @@ -949,10 +922,6 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero parseOutput: false }); - localConfig.updateFunction(func['$id'], { - "$id": response['$id'], - }); - func["$id"] = response['$id']; updaterRow.update({ status: 'Created' }); } catch (e) { updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); @@ -960,43 +929,6 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero } } - if (func.variables) { - if (!func.pushVariables) { - updaterRow.update({ end: 'Skipping variables' }); - } else { - updaterRow.update({ end: 'Pushing variables' }); - - const { variables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - await Promise.all(variables.map(async variable => { - await functionsDeleteVariable({ - functionId: func['$id'], - variableId: variable['$id'], - parseOutput: false - }); - })); - - let result = await awaitPools.wipeVariables(func['$id']); - if (!result) { - updaterRow.fail({ errorMessage: 'Variable deletion timed out' }) - return; - } - - // Push local variables - await Promise.all(Object.keys(func.variables).map(async localVariableKey => { - await functionsCreateVariable({ - functionId: func['$id'], - key: localVariableKey, - value: func.variables[localVariableKey], - parseOutput: false - }); - })); - } - } - try { updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); response = await functionsCreateDeployment({ @@ -1082,27 +1014,25 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero })); Spinner.stop(); - console.log('\n'); failedDeployments.forEach((failed) => { const { name, deployment, $id } = failed; const failUrl = `${globalConfig.getEndpoint().replace('/v1', '')}/console/project-${localConfig.getProject().projectId}/functions/function-${$id}/deployment-${deployment}`; error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); - }) - - let message = chalk.green(`Pushed and deployed ${successfullyPushed} functions`); + }); if (!async) { - if (successfullyDeployed < successfullyPushed) { - message = `${chalk.green(`Pushed and deployed ${successfullyPushed} functions.`)} ${chalk.red(`${successfullyPushed - successfullyDeployed} failed to deploy`)}`; + if(successfullyPushed === 0) { + error('No functions were pushed.'); + } else if(successfullyDeployed != successfullyPushed) { + warn(`Successfully pushed ${successfullyDeployed} of ${successfullyPushed} functions`) } else { - if (successfullyPushed === 0) { - message = chalk.red(`Error pushing ${functions.length} functions`) - } + success(`Successfully pushed ${successfullyPushed} functions.`); } + } else { + success(`Successfully pushed ${successfullyPushed} functions.`); } - log(message); } const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { @@ -1111,12 +1041,9 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { if (cliConfig.all) { checkDeployConditions(localConfig); if (localConfig.getCollections().length === 0) { - if (returnOnZero) { - log('No collections found, skipping'); - return; - } - - throw new Error("No collections found in the current directory. Use 'appwrite pull collections' to synchronize existing one, or use 'appwrite init collection' to create a new one."); + log("No collections found."); + hint("Use 'appwrite pull collections' to synchronize existing one, or use 'appwrite init collection' to create a new one."); + return; } collections.push(...localConfig.getCollections()); } else { @@ -1131,7 +1058,8 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { }) } const databases = Array.from(new Set(collections.map(collection => collection['databaseId']))); - log('Checking for databases and collection changes'); + + log('Checking for changes ...'); // Parallel db actions await Promise.all(databases.map(async (databaseId) => { @@ -1153,7 +1081,7 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { success(`Updated ${localDatabase.name} ( ${databaseId} ) name`); } } catch (err) { - log(`Database ${databaseId} not found. Creating it now...`); + log(`Database ${databaseId} not found. Creating it now ...`); await databasesCreate({ databaseId: databaseId, @@ -1243,11 +1171,9 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { if (cliConfig.all) { checkDeployConditions(localConfig); if (configBuckets.length === 0) { - if (returnOnZero) { - log('No buckets found, skipping'); - return; - } - throw new Error("No buckets found in the current directory. Use 'appwrite pull buckets' to synchronize existing one, or use 'appwrite init bucket' to create a new one."); + log("No buckets found."); + hint("Use 'appwrite pull buckets' to synchronize existing one, or use 'appwrite init bucket' to create a new one."); + return; } bucketIds.push(...configBuckets.map((b) => b.$id)); } @@ -1264,8 +1190,10 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { buckets.push(...idBuckets); } + log('Pushing buckets ...'); + for (let bucket of buckets) { - log(`Pushing bucket ${bucket.name} ( ${bucket['$id']} )`) + log(`Pushing bucket ${chalk.bold(bucket['name'])} ...`); try { response = await storageGetBucket({ @@ -1273,8 +1201,6 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { parseOutput: false, }) - log(`Updating bucket ...`) - await storageUpdateBucket({ bucketId: bucket['$id'], name: bucket.name, @@ -1288,8 +1214,6 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { compression: bucket.compression, parseOutput: false }); - - success(`Pushed ${bucket.name} ( ${bucket['$id']} )`); } catch (e) { if (Number(e.code) === 404) { log(`Bucket ${bucket.name} does not exist in the project. Creating ... `); @@ -1307,13 +1231,13 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { antivirus: bucket.antivirus, parseOutput: false }) - - success(`Pushed ${bucket.name} ( ${bucket['$id']} )`); } else { throw e; } } } + + success(`Successfully pushed ${buckets.length} buckets.`); } const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { @@ -1325,11 +1249,8 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { if (cliConfig.all) { checkDeployConditions(localConfig); if (configTeams.length === 0) { - if (returnOnZero) { - log('No teams found, skipping'); - return; - } - throw new Error("No teams found in the current directory. Use 'appwrite pull teams' to synchronize existing one, or use 'appwrite init team' to create a new one."); + log("No teams found."); + hint("Use 'appwrite pull teams' to synchronize existing one, or use 'appwrite init team' to create a new one."); } teamIds.push(...configTeams.map((t) => t.$id)); } @@ -1346,8 +1267,10 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { teams.push(...idTeams); } + log('Pushing teams ...'); + for (let team of teams) { - log(`Pushing team ${team.name} ( ${team['$id']} )`) + log(`Pushing team ${chalk.bold(team['name'])} ...`); try { response = await teamsGet({ @@ -1355,15 +1278,11 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { parseOutput: false, }) - log(`Updating team ...`) - await teamsUpdateName({ teamId: team['$id'], name: team.name, parseOutput: false }); - - success(`Pushed ${team.name} ( ${team['$id']} )`); } catch (e) { if (Number(e.code) === 404) { log(`Team ${team.name} does not exist in the project. Creating ... `); @@ -1373,13 +1292,13 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { name: team.name, parseOutput: false }) - - success(`Pushed ${team.name} ( ${team['$id']} )`); } else { throw e; } } } + + success(`Successfully pushed ${teams.length} teams.`); } const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => { @@ -1392,11 +1311,8 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => if (cliConfig.all) { checkDeployConditions(localConfig); if (configTopics.length === 0) { - if (returnOnZero) { - log('No topics found, skipping'); - return; - } - throw new Error("No topics found in the current directory. Use 'appwrite pull topics' to synchronize existing one, or use 'appwrite init topic' to create a new one."); + log("No topics found."); + hint("Use 'appwrite pull topics' to synchronize existing one, or use 'appwrite init topic' to create a new one."); } topicsIds.push(...configTopics.map((b) => b.$id)); } @@ -1420,8 +1336,10 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => } } + log('Pushing topics ...'); + for (let topic of topics) { - log(`Pushing topic ${topic.name} ( ${topic['$id']} )`) + log(`Pushing topic ${chalk.bold(topic['name'])} ...`); try { response = await messagingGetTopic({ @@ -1435,16 +1353,12 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => continue; } - log(`Updating Topic ...`) - await messagingUpdateTopic({ topicId: topic['$id'], name: topic.name, subscribe: topic.subscribe, parseOutput: false }); - - success(`Pushed ${topic.name} ( ${topic['$id']} )`); } catch (e) { if (Number(e.code) === 404) { log(`Topic ${topic.name} does not exist in the project. Creating ... `); @@ -1462,6 +1376,8 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => } } } + + success(`Successfully pushed ${topics.length} topics.`); } const push = new Command("push") @@ -1477,9 +1393,9 @@ push })); push - .command("project") + .command("settings") .description("Push project name, services and auth settings") - .action(actionRunner(pushProject)); + .action(actionRunner(pushSettings)); push .command("function") @@ -1513,6 +1429,13 @@ push .description("Push messaging topics in the current project.") .action(actionRunner(pushMessagingTopic)); +const deploy = new Command("deploy") + .description(commandDescriptions['push']) + .action(actionRunner(async () => { + warn("Did you mean to run 'appwrite push' command?"); + })); + module.exports = { - push + push, + deploy } diff --git a/lib/commands/run.js b/lib/commands/run.js index 5d05e21..425e6d6 100644 --- a/lib/commands/run.js +++ b/lib/commands/run.js @@ -1,10 +1,8 @@ const Tail = require('tail').Tail; -const EventEmitter = require('node:events'); +const chalk = require('chalk'); const ignore = require("ignore"); const tar = require("tar"); const fs = require("fs"); -const ID = require("../id"); -const childProcess = require('child_process'); const chokidar = require('chokidar'); const inquirer = require("inquirer"); const path = require("path"); @@ -12,13 +10,11 @@ const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { paginate } = require('../paginate'); const { functionsListVariables } = require('./functions'); -const { usersGet, usersCreateJWT } = require('./users'); -const { projectsCreateJWT } = require('./projects'); const { questionsRunFunctions } = require("../questions"); -const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); +const { actionRunner, success, log, warn, error, hint, commandDescriptions, drawTable } = require("../parser"); const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); -const { openRuntimesVersion, runtimeNames, systemTools, JwtManager, Queue } = require('../emulation/utils'); -const { dockerStop, dockerCleanup, dockerStart, dockerBuild, dockerPull, dockerStopActive } = require('../emulation/docker'); +const { runtimeNames, systemTools, JwtManager, Queue } = require('../emulation/utils'); +const { dockerStop, dockerCleanup, dockerStart, dockerBuild, dockerPull } = require('../emulation/docker'); const runFunction = async ({ port, functionId, noVariables, noReload, userId } = {}) => { // Selection @@ -68,7 +64,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = } if(!portFound) { - error('Could not find an available port. Please select a port with `appwrite run --port YOUR_PORT` command.'); + error("Could not find an available port. Please select a port with 'appwrite run --port YOUR_PORT' command."); return; } } @@ -86,9 +82,9 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = commands: func.commands, }; - log("Local function configuration:"); drawTable([settings]); - log('If you wish to change your local settings, update the appwrite.json file and rerun the `appwrite run` command.'); + log("If you wish to change your local settings, update the appwrite.json file and rerun the 'appwrite run' command."); + hint("Permissions, events, CRON and timeouts dont apply when running locally."); await dockerCleanup(); @@ -116,22 +112,17 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = const variables = {}; if(!noVariables) { - if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); - } else { - try { - const { variables: remoteVariables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - remoteVariables.forEach((v) => { - variables[v.key] = v.value; - }); - } catch(err) { - log("Could not fetch remote variables: " + err.message); - log("Function will run locally, but will not have your function's environment variables set."); - } + try { + const { variables: remoteVariables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); + + remoteVariables.forEach((v) => { + variables[v.key] = v.value; + }); + } catch(err) { + warn("Remote variables not fetched. Production environment variables will not be avaiable. Reason: " + err.message); } } @@ -143,7 +134,11 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = runtimeNames[runtimeName] ?? ''; variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = func.runtime; - await JwtManager.setup(userId); + try { + await JwtManager.setup(userId); + } catch(err) { + warn("Dynamic API key not generated. Header x-appwrite-key will not be set. Reason: " + err.message); + } const headers = {}; headers['x-appwrite-key'] = JwtManager.functionJwt ?? ''; @@ -155,13 +150,17 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = await dockerPull(func); await dockerBuild(func, variables); + + log('Starting function using Docker ...'); + hint('Function automatically restarts when you edit your code.'); + await dockerStart(func, variables, port); new Tail(logsPath).on("line", function(data) { - console.log(data); + process.stdout.write(chalk.blackBright(`${data}\n`)); }); new Tail(errorsPath).on("line", function(data) { - console.log(data); + process.stdout.write(chalk.blackBright(`${data}\n`)); }); if(!noReload) { @@ -177,23 +176,16 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = Queue.events.on('reload', async ({ files }) => { Queue.lock(); - log('Live-reloading due to file changes: '); - for(const file of files) { - log(`- ${file}`); - } - try { - log('Stopping the function ...'); - - await dockerStopActive(); + await dockerStop(func.$id); const dependencyFile = files.find((filePath) => tool.dependencyFiles.includes(filePath)); if(tool.isCompiled || dependencyFile) { - log(`Rebuilding the function due to cange in ${dependencyFile} ...`); + log(`Rebuilding the function ...`); await dockerBuild(func, variables); await dockerStart(func, variables, port); } else { - log('Hot-swapping function files ...'); + log('Hot-swapping function.. Files with change are ' + files.join(', ')); const functionPath = path.join(process.cwd(), func.path); const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); diff --git a/lib/config.js b/lib/config.js index ca66b52..af4c95d 100644 --- a/lib/config.js +++ b/lib/config.js @@ -4,6 +4,38 @@ const _path = require("path"); const process = require("process"); const JSONbig = require("json-bigint")({ storeAsString: false }); +const KeysFunction = ["path", "$id", "execute", "name", "enabled", "logging", "runtime", "scopes", "events", "schedule", "timeout", "entrypoint", "commands"]; +const KeysDatabase = ["$id", "name", "enabled"]; +const KeysCollection = ["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "attributes", "indexes"]; +const KeysStorage = ["$id", "$permissions", "fileSecurity", "name", "enabled", "maximumFileSize", "allowedFileExtensions", "compression", "encryption", "antivirus"]; +const KeyTopics = ["$id", "name", "subscribe"]; +const KeyAttributes = ["key", "type", "required", "array", "size", "default"]; +const KeyIndexes = ["key", "type", "status", "attributes", "orders"]; + +function whitelistKeys(value, keys, nestedKeys = []) { + if(Array.isArray(value)) { + const newValue = []; + + for(const item of value) { + newValue.push(whitelistKeys(item, keys, nestedKeys)); + } + + return newValue; + } + + const newValue = {}; + Object.keys(value).forEach((key) => { + if(keys.includes(key)) { + if(nestedKeys[key]) { + newValue[key] = whitelistKeys(value[key], nestedKeys[key]); + } else { + newValue[key] = value[key]; + } + } + }); + return newValue; +} + class Config { constructor(path) { this.path = path; @@ -94,6 +126,8 @@ class Local extends Config { } addFunction(props) { + props = whitelistKeys(props, KeysFunction); + if (!this.has("functions")) { this.set("functions", []); } @@ -101,21 +135,6 @@ class Local extends Config { let functions = this.get("functions"); for (let i = 0; i < functions.length; i++) { if (functions[i]['$id'] == props['$id']) { - return; - } - } - functions.push(props); - this.set("functions", functions); - } - - updateFunction(id, props) { - if (!this.has("functions")) { - return; - } - - let functions = this.get("functions"); - for (let i = 0; i < functions.length; i++) { - if (functions[i]['$id'] == id) { functions[i] = { ...functions[i], ...props @@ -124,6 +143,9 @@ class Local extends Config { return; } } + + functions.push(props); + this.set("functions", functions); } getCollections() { @@ -149,6 +171,11 @@ class Local extends Config { } addCollection(props) { + props = whitelistKeys(props, KeysCollection, { + attributes: KeyAttributes, + indexes: KeyIndexes + }); + if (!this.has("collections")) { this.set("collections", []); } @@ -188,6 +215,8 @@ class Local extends Config { } addBucket(props) { + props = whitelistKeys(props, KeysStorage); + if (!this.has("buckets")) { this.set("buckets", []); } @@ -227,6 +256,8 @@ class Local extends Config { } addMessagingTopic(props) { + props = whitelistKeys(props, KeyTopics); + if (!this.has("topics")) { this.set("topics", []); } @@ -266,6 +297,8 @@ class Local extends Config { } addDatabase(props) { + props = whitelistKeys(props, KeysDatabase); + if (!this.has("databases")) { this.set("databases", []); } @@ -329,7 +362,7 @@ class Local extends Config { return { projectId: this.get("projectId"), projectName: this.get("projectName"), - projectSettings: this.get('projectSettings') + projectSettings: this.get('settings') }; } @@ -357,7 +390,6 @@ class Local extends Config { functions: projectSettings.serviceStatusForFunctions, graphql: projectSettings.serviceStatusForGraphql, messaging: projectSettings.serviceStatusForMessaging, - }, auth: { methods: { @@ -380,7 +412,7 @@ class Local extends Config { } }; - this.set('projectSettings', settings) + this.set('settings', settings) } } @@ -520,7 +552,7 @@ class Global extends Config { const current = this.getCurrentSession(); if (current) { - const config = this.get(current); + const config = this.get(current) ?? {}; return config[key] !== undefined; } @@ -530,7 +562,7 @@ class Global extends Config { const current = this.getCurrentSession(); if (current) { - const config = this.get(current); + const config = this.get(current) ?? {}; return config[key]; } diff --git a/lib/emulation/docker.js b/lib/emulation/docker.js index fe50339..2dde55f 100644 --- a/lib/emulation/docker.js +++ b/lib/emulation/docker.js @@ -1,15 +1,12 @@ +const chalk = require('chalk'); const childProcess = require('child_process'); const { localConfig } = require("../config"); const path = require('path'); const fs = require('fs'); -const { log,success } = require("../parser"); +const { log, success, hint } = require("../parser"); const { openRuntimesVersion, systemTools } = require("./utils"); -const ID = require("../id"); - -const activeDockerIds = {}; async function dockerStop(id) { - delete activeDockerIds[id]; const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { stdio: 'pipe', }); @@ -18,20 +15,42 @@ async function dockerStop(id) { } async function dockerPull(func) { - log('Pulling Docker image of function runtime ...'); - const runtimeChunks = func.runtime.split("-"); const runtimeVersion = runtimeChunks.pop(); const runtimeName = runtimeChunks.join("-"); const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; - const pullProcess = childProcess.spawn('docker', ['pull', imageName], { + const checkProcess = childProcess.spawn('docker', ['images', '--format', 'json', imageName], { stdio: 'pipe', pwd: path.join(process.cwd(), func.path) }); - pullProcess.stderr.on('data', (data) => { - process.stderr.write(`\n${data}$ `); + let hasImage = false; + + checkProcess.stdout.on('data', (data) => { + if(data) { + hasImage = false; + } + }); + + checkProcess.stderr.on('data', (data) => { + if(data) { + hasImage = false; + } + }); + + await new Promise((res) => { checkProcess.on('close', res) }); + + if(hasImage) { + return; + } + + log('Pulling Docker image ...'); + hint('This may take a few minutes, but we only need to do this once.'); + + const pullProcess = childProcess.spawn('docker', ['pull', imageName], { + stdio: 'pipe', + pwd: path.join(process.cwd(), func.path) }); await new Promise((res) => { pullProcess.on('close', res) }); @@ -47,7 +66,7 @@ async function dockerBuild(func, variables) { const functionDir = path.join(process.cwd(), func.path); - const id = ID.unique(); + const id = func.$id; const params = [ 'run' ]; params.push('--name', id); @@ -55,6 +74,7 @@ async function dockerBuild(func, variables) { params.push('-e', 'APPWRITE_ENV=development'); params.push('-e', 'OPEN_RUNTIMES_ENV=development'); params.push('-e', 'OPEN_RUNTIMES_SECRET='); + params.push('-l', 'appwrite-env=dev'); params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`); for(const k of Object.keys(variables)) { @@ -69,11 +89,11 @@ async function dockerBuild(func, variables) { }); buildProcess.stdout.on('data', (data) => { - process.stdout.write(`\n${data}`); + process.stdout.write(chalk.blackBright(`${data}\n`)); }); buildProcess.stderr.on('data', (data) => { - process.stderr.write(`\n${data}`); + process.stderr.write(chalk.blackBright(`${data}\n`)); }); await new Promise((res) => { buildProcess.on('close', res) }); @@ -91,14 +111,7 @@ async function dockerBuild(func, variables) { await new Promise((res) => { copyProcess.on('close', res) }); - const cleanupProcess = childProcess.spawn('docker', ['rm', '--force', id], { - stdio: 'pipe', - pwd: functionDir - }); - - await new Promise((res) => { cleanupProcess.on('close', res) }); - - delete activeDockerIds[id]; + await dockerStop(id); const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); if (fs.existsSync(tempPath)) { @@ -107,15 +120,6 @@ async function dockerBuild(func, variables) { } async function dockerStart(func, variables, port) { - log('Starting function using Docker ...'); - - log("Permissions, events, CRON and timeouts dont apply when running locally."); - - log('💡 Hint: Function automatically restarts when you edit your code.'); - - success(`Visit http://localhost:${port}/ to execute your function.`); - - const runtimeChunks = func.runtime.split("-"); const runtimeVersion = runtimeChunks.pop(); const runtimeName = runtimeChunks.join("-"); @@ -125,13 +129,14 @@ async function dockerStart(func, variables, port) { const functionDir = path.join(process.cwd(), func.path); - const id = ID.unique(); + const id = func.$id; const params = [ 'run' ]; params.push('--rm'); params.push('-d'); params.push('--name', id); params.push('-p', `${port}:3000`); + params.push('-l', 'appwrite-env=dev'); params.push('-e', 'APPWRITE_ENV=development'); params.push('-e', 'OPEN_RUNTIMES_ENV=development'); params.push('-e', 'OPEN_RUNTIMES_SECRET='); @@ -150,11 +155,11 @@ async function dockerStart(func, variables, port) { pwd: functionDir }); - activeDockerIds[id] = true; + success(`Visit http://localhost:${port}/ to execute your function.`); } async function dockerCleanup() { - await dockerStop(); + await dockerStopActive(); const functions = localConfig.getFunctions(); for(const func of functions) { @@ -171,17 +176,40 @@ async function dockerCleanup() { } async function dockerStopActive() { - const ids = Object.keys(activeDockerIds); - for await (const id of ids) { + const listProcess = childProcess.spawn('docker', ['ps', '-a', '-q', '--filter', 'label=appwrite-env=dev'], { + stdio: 'pipe', + }); + + const ids = []; + function handleOutput(data) { + const list = data.toString().split('\n'); + for(const id of list) { + if(id && !id.includes(' ')) { + ids.push(id); + } + } + } + + listProcess.stdout.on('data', (data) => { + handleOutput(data); + }); + + listProcess.stderr.on('data', (data) => { + handleOutput(data); + }); + + await new Promise((res) => { listProcess.on('close', res) }); + + for(const id of ids) { await dockerStop(id); } } module.exports = { - dockerStop, dockerPull, dockerBuild, dockerStart, dockerCleanup, dockerStopActive, + dockerStop, } diff --git a/lib/emulation/utils.js b/lib/emulation/utils.js index d36d5cd..496aa55 100644 --- a/lib/emulation/utils.js +++ b/lib/emulation/utils.js @@ -2,8 +2,7 @@ const EventEmitter = require('node:events'); const { projectsCreateJWT } = require('../commands/projects'); const { localConfig } = require("../config"); - -const openRuntimesVersion = 'v3'; +const openRuntimesVersion = 'v4'; const runtimeNames = { 'node': 'Node.js', @@ -17,7 +16,8 @@ const runtimeNames = { 'java': 'Java', 'swift': 'Swift', 'kotlin': 'Kotlin', - 'bun': 'Bun' + 'bun': 'Bun', + 'go': 'Go', }; const systemTools = { @@ -81,6 +81,11 @@ const systemTools = { startCommand: "bun src/server.ts", dependencyFiles: [ "package.json", "package-lock.json", "bun.lockb" ] }, + 'go': { + isCompiled: true, + startCommand: "src/function/server", + dependencyFiles: [ ] + }, }; const JwtManager = { diff --git a/lib/parser.js b/lib/parser.js index 31e6850..a8d75b4 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -131,7 +131,7 @@ const parseError = (err) => { } catch { } - const version = '6.0.0-rc.1'; + const version = '6.0.0-rc.2'; const stepsToReproduce = `Running \`appwrite ${cliConfig.reportData.data.args.join(' ')}\``; const yourEnvironment = `CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`; @@ -189,15 +189,23 @@ const parseBool = (value) => { } const log = (message) => { - console.log(`${chalk.cyan.bold("ℹ Info")} ${chalk.cyan(message ?? "")}`); + console.log(`${chalk.cyan.bold("ℹ Info:")} ${chalk.cyan(message ?? "")}`); +} + +const warn = (message) => { + console.log(`${chalk.yellow.bold("ℹ Warning:")} ${chalk.yellow(message ?? "")}`); +} + +const hint = (message) => { + console.log(`${chalk.cyan.bold("♥ Hint:")} ${chalk.cyan(message ?? "")}`); } const success = (message) => { - console.log(`${chalk.green.bold("✓ Success")} ${chalk.green(message ?? "")}`); + console.log(`${chalk.green.bold("✓ Success:")} ${chalk.green(message ?? "")}`); } const error = (message) => { - console.error(`${chalk.red.bold("✗ Error")} ${chalk.red(message ?? "")}`); + console.error(`${chalk.red.bold("✗ Error:")} ${chalk.red(message ?? "")}`); } const logo = "\n _ _ _ ___ __ _____\n \/_\\ _ __ _ ____ ___ __(_) |_ ___ \/ __\\ \/ \/ \\_ \\\n \/\/_\\\\| '_ \\| '_ \\ \\ \/\\ \/ \/ '__| | __\/ _ \\ \/ \/ \/ \/ \/ \/\\\/\n \/ _ \\ |_) | |_) \\ V V \/| | | | || __\/ \/ \/___\/ \/___\/\\\/ \/_\n \\_\/ \\_\/ .__\/| .__\/ \\_\/\\_\/ |_| |_|\\__\\___| \\____\/\\____\/\\____\/\n |_| |_|\n\n"; @@ -221,8 +229,8 @@ const commandDescriptions = { "client": `The client command allows you to configure your CLI`, "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your Appwrite account.`, - "whoami": `The whoami command gives information about the currently logged in user.`, - "register": `Outputs the link to create an Appwrite account..`, + "whoami": `The whoami command gives information about the currently signed in user.`, + "register": `Outputs the link to create an Appwrite account.`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, "messaging": `The messaging command allows you to send messages.`, @@ -240,6 +248,8 @@ module.exports = { parseInteger, parseBool, log, + warn, + hint, success, error, commandDescriptions, diff --git a/lib/questions.js b/lib/questions.js index d003f9d..b347469 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -46,7 +46,7 @@ const getIgnores = (runtime) => { return ['.build', '.swiftpm']; } - return undefined; + return []; }; const getEntrypoint = (runtime) => { @@ -80,6 +80,8 @@ const getEntrypoint = (runtime) => { return 'src/Main.java'; case 'kotlin': return 'src/Main.kt'; + case 'go': + return 'main.go'; } return undefined; @@ -209,18 +211,26 @@ const questionsInitProject = [ when: (answer) => answer.start === 'existing' } ]; +const questionsInitProjectAutopull = [ + { + type: "confirm", + name: "autopull", + message: + `Would you like to pull all resources from project you just linked?` + }, +]; const questionsPullResources = [ { type: "list", name: "resource", message: "Which resources would you like to pull?", choices: [ - { name: 'Project', value: 'project' }, - { name: 'Functions', value: 'functions' }, - { name: 'Collections', value: 'collections' }, - { name: 'Buckets', value: 'buckets' }, - { name: 'Teams', value: 'teams' }, - { name: 'Topics', value: 'messages' } + { name: `Settings ${chalk.blackBright(`(Project)`)}`, value: 'settings' }, + { name: `Functions ${chalk.blackBright(`(Deployment)`)}`, value: 'functions' }, + { name: `Collections ${chalk.blackBright(`(Databases)`)}`, value: 'collections' }, + { name: `Buckets ${chalk.blackBright(`(Storage)`)}`, value: 'buckets' }, + { name: `Teams ${chalk.blackBright(`(Auth)`)}`, value: 'teams' }, + { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' } ] } ] @@ -283,8 +293,23 @@ const questionsCreateFunction = [ } }) return choices; - } - } + }, + }, + { + type: "list", + name: "template", + message: "How would you like to start your function code?", + choices: [ + { + name: `Start from scratch ${chalk.blackBright(`(starter)`)}`, + value: "starter" + }, + { + name: "Pick a template", + value: "custom" + } + ] + }, ]; const questionsCreateFunctionSelectTemplate = (templates) => { @@ -451,12 +476,12 @@ const questionsLogin = [ { type: "list", name: "method", - message: "You're already logged in, what you like to do?", + message: "What you like to do?", choices: [ - { name: 'Login to a different account', value: 'login' }, - { name: 'Change to a different existed account', value: 'select' } + { name: 'Login to an account', value: 'login' }, + { name: 'Switch to an account', value: 'select' } ], - when: () => globalConfig.getCurrentSession() !== '' + when: () => globalConfig.getSessions().length >= 2 }, { type: "input", @@ -570,12 +595,12 @@ const questionsPushResources = [ name: "resource", message: "Which resources would you like to push?", choices: [ - { name: 'Project', value: 'project' }, - { name: 'Functions', value: 'functions' }, - { name: 'Collections', value: 'collections' }, - { name: 'Buckets', value: 'buckets' }, - { name: 'Teams', value: 'teams' }, - { name: 'Topics', value: 'messages' } + { name: `Settings ${chalk.blackBright(`(Project)`)}`, value: 'settings' }, + { name: `Functions ${chalk.blackBright(`(Deployment)`)}`, value: 'functions' }, + { name: `Collections ${chalk.blackBright(`(Databases)`)}`, value: 'collections' }, + { name: `Buckets ${chalk.blackBright(`(Storage)`)}`, value: 'buckets' }, + { name: `Teams ${chalk.blackBright(`(Auth)`)}`, value: 'teams' }, + { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' } ] } ]; @@ -605,7 +630,7 @@ const questionsPushFunctions = [ let functions = localConfig.getFunctions(); checkDeployConditions(localConfig) if (functions.length === 0) { - throw new Error("No functions found in the current directory Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); + throw new Error("No functions found Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); } let choices = functions.map((func, idx) => { return { @@ -794,7 +819,7 @@ const questionsRunFunctions = [ choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { - throw new Error("No functions found in the current directory. Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); + throw new Error("No functions found. Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); } let choices = functions.map((func, idx) => { return { @@ -809,6 +834,7 @@ const questionsRunFunctions = [ module.exports = { questionsInitProject, + questionsInitProjectAutopull, questionsCreateFunction, questionsCreateFunctionSelectTemplate, questionsCreateBucket, diff --git a/lib/sdks.js b/lib/sdks.js index 48957b4..f8cb8fc 100644 --- a/lib/sdks.js +++ b/lib/sdks.js @@ -1,44 +1,12 @@ -const inquirer = require("inquirer"); const Client = require("./client"); const { globalConfig, localConfig } = require("./config"); -const questionGetEndpoint = [ - { - type: "input", - name: "endpoint", - message: "Enter the endpoint of your Appwrite server", - default: "http://localhost/v1", - async validate(value) { - if (!value) { - return "Please enter a valid endpoint."; - } - let client = new Client().setEndpoint(value); - try { - let response = await client.call('get', '/health/version'); - if (response.version) { - return true; - } else { - throw new Error(); - } - } catch (error) { - return "Invalid endpoint or your Appwrite server is not running as expected."; - } - } - } -] - const sdkForConsole = async (requiresAuth = true) => { let client = new Client(); let endpoint = globalConfig.getEndpoint(); let cookie = globalConfig.getCookie() let selfSigned = globalConfig.getSelfSigned() - if (!endpoint) { - const answers = await inquirer.prompt(questionGetEndpoint) - endpoint = answers.endpoint; - globalConfig.setEndpoint(endpoint); - } - if (requiresAuth && cookie === "") { throw new Error("Session not found. Please run `appwrite login` to create a session"); } @@ -61,12 +29,6 @@ const sdkForProject = async () => { let cookie = globalConfig.getCookie() let selfSigned = globalConfig.getSelfSigned() - if (!endpoint) { - const answers = await inquirer.prompt(questionGetEndpoint) - endpoint = answers.endpoint; - globalConfig.setEndpoint(endpoint); - } - if (!project) { throw new Error("Project is not set. Please run `appwrite init` to initialize the current directory with an Appwrite project."); } diff --git a/package.json b/package.json index c293b91..0fe8b81 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "appwrite-cli", "homepage": "https://appwrite.io/support", "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API", - "version": "6.0.0-rc.1", + "version": "6.0.0-rc.2", "license": "BSD-3-Clause", "main": "index.js", "bin": { diff --git a/scoop/appwrite.json b/scoop/appwrite.json index 918f944..85d84c6 100644 --- a/scoop/appwrite.json +++ b/scoop/appwrite.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json", - "version": "6.0.0-rc.1", + "version": "6.0.0-rc.2", "description": "The Appwrite CLI is a command-line application that allows you to interact with Appwrite and perform server-side tasks using your terminal.", "homepage": "https://github.com/appwrite/sdk-for-cli", "license": "BSD-3-Clause", "architecture": { "64bit": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.1/appwrite-cli-win-x64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.2/appwrite-cli-win-x64.exe", "bin": [ [ "appwrite-cli-win-x64.exe", @@ -15,7 +15,7 @@ ] }, "arm64": { - "url": "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.1/appwrite-cli-win-arm64.exe", + "url": "https://github.com/appwrite/sdk-for-cli/releases/download/6.0.0-rc.2/appwrite-cli-win-arm64.exe", "bin": [ [ "appwrite-cli-win-arm64.exe",