diff --git a/src/commands/function/delete.ts b/src/commands/function/delete.ts index 7fde2d9..c2e2671 100644 --- a/src/commands/function/delete.ts +++ b/src/commands/function/delete.ts @@ -42,7 +42,7 @@ const deleteFunction = async (data: any) => { console.log(Chalk.yellow(`Deleting ${functionName} ...`)) console.log('') - const { data } = await consoleClient.get(`/api/modules/mine`, {}) + const { data } = await consoleClient.get(`/api/modules/mine?limit=999`, {}) const functions = data.docs ? data.docs : [] // Sort all matching functions by name and select the last matching function diff --git a/src/commands/function/deploy.ts b/src/commands/function/deploy.ts index 8a5a8b8..35b5f66 100644 --- a/src/commands/function/deploy.ts +++ b/src/commands/function/deploy.ts @@ -65,7 +65,7 @@ const deployFunction = async (functionName: string, functionData: any, options: // Find all matching functions, warn users if they are overwriting a deployed function try { - const { data } = await consoleClient.get(`/api/modules/mine`, {}) + const { data } = await consoleClient.get(`/api/modules/mine?limit=999`, {}) const functions = data.docs ? data.docs : [] // Sort all matching functions by name and select the last matching function diff --git a/src/commands/function/list.ts b/src/commands/function/list.ts index 3a7f6c1..069e9c1 100644 --- a/src/commands/function/list.ts +++ b/src/commands/function/list.ts @@ -4,7 +4,7 @@ import { logger } from "../../lib/logger" export const run = async () => { try { - const { data } = await consoleClient.get(`/api/modules/mine`, {}) + const { data } = await consoleClient.get(`/api/modules/mine?limit=999`, {}) const functions = data.docs ? data.docs : [] logger.log('List of Functions:') diff --git a/src/commands/function/stop.ts b/src/commands/function/stop.ts index a74acaa..9e644fe 100644 --- a/src/commands/function/stop.ts +++ b/src/commands/function/stop.ts @@ -42,7 +42,7 @@ const stopFunction = async (data: any) => { console.log(Chalk.yellow(`Stopping ${functionName} ...`)) console.log('') - const { data } = await consoleClient.get(`/api/modules/mine`, {}) + const { data } = await consoleClient.get(`/api/modules/mine?limit=999`, {}) const functions = data.docs ? data.docs : [] // Sort all matching functions by name and select the last matching function diff --git a/src/commands/function/update.ts b/src/commands/function/update.ts index 147104f..bf83e5b 100644 --- a/src/commands/function/update.ts +++ b/src/commands/function/update.ts @@ -64,7 +64,7 @@ const updateFunction = async (data: any) => { // Find all matching functions, warn users if they are updating a function that is not deployed try { - const { data } = await consoleClient.get(`/api/modules/mine`, {}) + const { data } = await consoleClient.get(`/api/modules/mine?limit=999`, {}) const functions = data.docs ? data.docs : [] // Sort all matching functions by name and select the last matching function diff --git a/src/commands/sites/build.ts b/src/commands/sites/build.ts index 687c4e7..bd0c733 100644 --- a/src/commands/sites/build.ts +++ b/src/commands/sites/build.ts @@ -69,15 +69,4 @@ export const run = async (options: { logger.error('Failed to build site.', error.message) return } -} - -const dynamicImport = async (path: string) => { - console.log('dynamic import') - - try { - const module = await import(path) - return module.default - } catch (err) { - return console.error(err) - } -}; +} \ No newline at end of file diff --git a/src/commands/sites/delete.ts b/src/commands/sites/delete.ts new file mode 100644 index 0000000..84eeccf --- /dev/null +++ b/src/commands/sites/delete.ts @@ -0,0 +1,85 @@ +import Chalk from "chalk" +import { parseBlsConfig } from "../../lib/blsConfig" +import { consoleClient } from "../../lib/http" +import { logger } from "../../lib/logger" +import { normalizeFunctionName } from "../../lib/strings" + +interface DeleteCommandOptions { + target: string +} + +/** + * Entry function for bls site delete + * + * @param options + */ +export const run = async (options: DeleteCommandOptions) => { + try { + if (options.target) { + await deleteSite({ name: options.target }) + } else { + const { name: configName } = parseBlsConfig() + await deleteSite({ name: configName }) + } + } catch (error: any) { + logger.error('Failed to delete site.', error.message) + } +} + +/** + * + * @param data + * @returns + */ +const deleteSite = async (data: any) => { + const { name: functionName } = data + + let matchingFunction = null + let internalFunctionId = null + + // Find all matching functions, warn users if they are updating a function that is not deployed + try { + console.log(Chalk.yellow(`Deleting ${functionName} ...`)) + console.log('') + + const { data } = await consoleClient.get(`/api/sites?limit=999`, {}) + const functions = data.docs ? data.docs : [] + + // Sort all matching functions by name and select the last matching function + // TODO: Ensure all functions have unique names under a user's scope + const matchingFunctions = functions.filter((f: any) => + normalizeFunctionName(f.functionName) === normalizeFunctionName(functionName)) + + if (matchingFunctions && matchingFunctions.length > 0) { + matchingFunction = matchingFunctions[matchingFunctions.length - 1] + internalFunctionId = matchingFunction._id + } + + // If a function does not exist, request the user to deploy that function first + if (!matchingFunction) { + throw new Error('Site not found.') + } + } catch (error: any) { + logger.error('Failed to retrive deployed sites.', error.message) + return + } + + // Delete the site + try { + if (!internalFunctionId || !matchingFunction) + throw new Error('Unable to retrive site ID.') + + const { data } = await consoleClient.delete(`/api/sites/${internalFunctionId}`) + + if (!data) throw new Error("") + + console.log( + Chalk.green( + `Successfully deleted site ${functionName}!` + ) + ) + } catch (error: any) { + logger.error('Failed to delete site.', error.message) + return + } +} \ No newline at end of file diff --git a/src/commands/sites/deploy.ts b/src/commands/sites/deploy.ts new file mode 100644 index 0000000..562765a --- /dev/null +++ b/src/commands/sites/deploy.ts @@ -0,0 +1,132 @@ +import Chalk from "chalk" +import { run as runPublish } from "./publish" +import { basename, resolve } from "path" +import { consoleClient } from "../../lib/http" +import promptFnDeploy from "../../prompts/function/deploy" +import { parseBlsConfig } from "../../lib/blsConfig" +import { logger } from "../../lib/logger" +import { normalizeFunctionName, slugify } from "../../lib/strings" + +interface DeployCommandOptions { + name?: string + path?: string + yes?: boolean +} + +/** + * Entry function for bls function deploy + * + * @param options + */ +export const run = (options: DeployCommandOptions) => { + try { + const { + name: configName + } = parseBlsConfig() + + const { + name = configName || basename(resolve(process.cwd())), + path = process.cwd() + } = options + + runPublish({ + debug: false, + name, + path, + publishCallback: (data: any) => deployFunction(slugify(name), data, options), + rebuild: true, + }) + } catch (error: any) { + logger.error('Failed to deploy site.', error.message) + } +} + +/** + * Helper to deploy a bls site via CLI + * + * 1. Publish package and retrive ipfs site id + * 2. Get list of user sites + * 3. Decide whether to update or create a new site (based on site name), bail if deploying same data + * 4. Call new or update API with site config parameters + * 5. Run deploy + * + * @param data + * @returns + */ +const deployFunction = async (functionName: string, functionData: any, options: DeployCommandOptions) => { + const { cid: functionId } = functionData + let matchingFunction = null + let internalFunctionId = null + + // Find all matching functions, warn users if they are overwriting a deployed function + try { + const { data } = await consoleClient.get(`/api/sites?limit=999`, {}) + const functions = data.docs ? data.docs : [] + + // Sort all matching functions by name and select the last matching function + // TODO: Ensure all functions have unique names under a user's scope + const matchingFunctions = functions.filter((f: any) => + normalizeFunctionName(f.functionName) === normalizeFunctionName(functionName)) + + if (matchingFunctions && matchingFunctions.length > 0) { + matchingFunction = matchingFunctions[matchingFunctions.length - 1] + internalFunctionId = matchingFunction._id + } + + // If a function exists and has been deployed, request a user's confirmation + if (matchingFunction && matchingFunction.status === 'deployed' && !options.yes) { + const { confirm } = await promptFnDeploy({ name: matchingFunction.functionName }) + + if (!confirm) { + throw new Error("Cancelled by user, aborting deployment.") + } + } + } catch (error: any) { + logger.error('Failed to retrive deployed sites.', error.message) + return + } + + // Create or update site + try { + let response + + if (!internalFunctionId) { + response = await consoleClient.post(`/api/sites`, { functionId, functionName }) + } else { + response = await consoleClient.patch( + `/api/sites/${internalFunctionId}`, + { functionId, functionName, status: 'deploying' } + ) + } + + if (!internalFunctionId && response.data && response.data._id) internalFunctionId = response.data._id + } catch (error: any) { + logger.error('Failed to update site metadata.', error.message) + return + } + + // Deploy Site + try { + if (!internalFunctionId) throw new Error('Unable to retrive site ID') + console.log(Chalk.yellow(`Deploying ${functionName} ...`)) + + const { data } = await consoleClient.put(`/api/sites/${internalFunctionId}/deploy`, { + functionId: functionId + }) + + if (!!data.err) { + console.log(Chalk.red(`Deployment unsuccessful, ${data.message}`)) + } else { + console.log( + Chalk.green( + `Successfully deployed ${functionName} with id ${functionId}` + ) + ) + } + } catch (error: any) { + logger.error('Failed to deploy site.', error.message) + return + } + + return +} \ No newline at end of file diff --git a/src/commands/sites/index.ts b/src/commands/sites/index.ts index 271024c..014c741 100644 --- a/src/commands/sites/index.ts +++ b/src/commands/sites/index.ts @@ -1,13 +1,25 @@ import type { Argv } from "yargs" import { run as runInit } from "./init" +import { run as runList } from "./list" import { run as runBuild } from "./build" import { run as runPreview } from "./preview" +import { run as runDelete } from "./delete" +import { run as runDeploy } from "./deploy" export function sitesCli(yargs: Argv) { yargs .usage('bls sites [subcommand]') .demandOption('experimental') + yargs.command( + 'list', + 'Lists your deployed blockless sites', + () => { }, + () => { + runList() + } + ) + yargs.command( 'init [name]', 'Initializes a blockless site project with a given name and template', @@ -29,6 +41,22 @@ export function sitesCli(yargs: Argv) { } ) + yargs.command( + 'delete [target]', + 'Undeploys and deletes a site from the network', + (yargs) => { + return yargs + .positional('target', { + description: 'The name of the site to delete (Defaults to the working directory)', + type: 'string', + default: undefined + }) + }, + (argv) => { + runDelete(argv as any) + } + ) + yargs.command( 'build [path]', 'Builds and creates a wasm archive of a static site', @@ -67,4 +95,20 @@ export function sitesCli(yargs: Argv) { runPreview(argv) } ) + + yargs.command( + 'deploy [path]', + 'Deploys a static site on Blockless', + (yargs) => { + return yargs + .positional('path', { + description: 'Set the path to the static site project', + type: 'string', + default: undefined + }) + }, + (argv) => { + runDeploy(argv as any) + } + ) } \ No newline at end of file diff --git a/src/commands/sites/list.ts b/src/commands/sites/list.ts new file mode 100644 index 0000000..a3b9cca --- /dev/null +++ b/src/commands/sites/list.ts @@ -0,0 +1,31 @@ +import Chalk from "chalk" +import { consoleClient } from "../../lib/http" +import { logger } from "../../lib/logger" + +export const run = async () => { + try { + const { data } = await consoleClient.get(`/api/sites?limit=999`, {}) + const sites = data.docs ? data.docs : [] + + logger.log('List of Sites:') + logger.log('-----------------------------------') + + if (sites && sites.length > 0) { + sites.forEach && sites.forEach((f: any) => { + logger.log('') + logger.log(`${Chalk.blue('Name:')} ${f.functionName}`) + logger.log(`${Chalk.blue('CID:')} ${f.functionId}`) + logger.log(`${Chalk.blue('Status:')} ${f.status === 'stopped' ? Chalk.red(f.status) : f.status === 'deployed' ? Chalk.green(f.status) : f.status}`) + }) + + logger.log('') + logger.log(`Total Sites: ${sites.length}`) + } else { + logger.log('') + logger.log('You have no sites.') + } + } catch (error: any) { + logger.error('Failed to retrieve site list.', error.message) + return + } +} \ No newline at end of file diff --git a/src/commands/sites/publish.ts b/src/commands/sites/publish.ts new file mode 100644 index 0000000..1d791b4 --- /dev/null +++ b/src/commands/sites/publish.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "fs"; +import Chalk from "chalk"; +import FormData from "form-data"; +import axios from "axios"; +import { getToken } from "../../store/db"; +import { run as runBuild } from "./build"; +import { resolve } from "path"; +import { getWASMRepoServer } from "../../lib/urls"; +import { parseBlsConfig } from "../../lib/blsConfig" +import { logger } from "../../lib/logger" +import { slugify } from "../../lib/strings" + +export const publishSite = async ( + manifest: any, + archive: any, + archiveName: string, + cb?: Function +) => { + const server = getWASMRepoServer(); + const token = getToken(); + const formData = new FormData(); + + formData.append("manifest", manifest, "manifest.json"); + formData.append("wasi_archive", archive, archiveName); + + axios + .post(`${server}/api/submit`, formData, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "multipart/form-data", + }, + }) + .then((res) => { + console.log(Chalk.green('Publish successful!')) + console.log('') + + if (cb) { + cb(res.data); + } + }) + .catch((error) => { + logger.error('Failed to publish site.', error.message) + }); +}; + +const logResult = (data: any) => { + const { cid } = data; + console.log(`Site successfully published with id ${cid}`); +}; +export const run = (options: any) => { + const { + debug = true, + path = process.cwd(), + publishCallback = logResult, + rebuild, + } = options; + + // Fetch BLS config + const { name, build, build_release } = parseBlsConfig() + const buildConfig = !debug ? build_release : build + const buildName = buildConfig.entry ? slugify(buildConfig.entry.replace('.wasm', '')) : slugify(name) + const buildDir = resolve(path, buildConfig.dir || 'build') + const wasmArchive = `${slugify(buildName)}.tar.gz` + + // Run the build command + runBuild({ debug, path, rebuild }); + + console.log(`${Chalk.yellow('Publishing:')} site located in ${buildDir}`); + + publishSite( + readFileSync(resolve(buildDir, 'manifest.json')), + readFileSync(resolve(buildDir, wasmArchive)), + wasmArchive, + publishCallback + ); +};