Skip to content

[WIP] CLI changes for init, push, pull sites #1071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions templates/cli/lib/commands/init.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { localConfig, globalConfig } = require("../config");
const {
questionsCreateFunction,
questionsCreateFunctionSelectTemplate,
questionsCreateSite,
questionsCreateBucket,
questionsCreateMessagingTopic,
questionsCreateCollection,
Expand All @@ -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,
Expand Down Expand Up @@ -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));
Expand All @@ -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")
Expand Down
125 changes: 124 additions & 1 deletion templates/cli/lib/commands/pull.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
Loading