diff --git a/.gitignore b/.gitignore index 72c9bf2..1178857 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,12 @@ typings/ # JetBrains .idea/ + +# Cli configs +nomo_cli.config.js + +# Manifest +out/nomo_manifest.json + +# Webon Files +out/nomo.tar.gz \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8c0cfee..19fe1dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nomo-webon-cli", - "version": "0.1.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nomo-webon-cli", - "version": "0.1.1", + "version": "0.1.0", "dependencies": { "commander": "^6.1.0", "dotenv": "^16.3.1", @@ -1163,6 +1163,24 @@ "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "dev": true }, + "node_modules/@types/ssh2": { + "version": "1.11.17", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.17.tgz", + "integrity": "sha512-owd0cyrAzaUICWvgpvwg7mMZcPvY4pH2Zs3fg0dgT+I/O9+kFJ4i+AUrX82INWJtEHbX70ubnSVqWJvt2VxmqQ==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.11.tgz", + "integrity": "sha512-c1vku6qnTeujJneYH94/4aq73XrVcsJe35UPyAsSok1ijiKrkRzK+AxQPSpNMUnC03roWBBwJx/9I8V7lQoxmA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1270,6 +1288,14 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1392,6 +1418,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1473,6 +1507,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1654,6 +1697,20 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cpu-features": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.17.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -3102,6 +3159,12 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3795,6 +3858,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -3828,6 +3896,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/src/build-webon/build-webon.ts b/src/build-webon/build-webon.ts index ec37315..33e1694 100644 --- a/src/build-webon/build-webon.ts +++ b/src/build-webon/build-webon.ts @@ -1,24 +1,19 @@ import { joinDirWithFileName, logFatal } from "../util/util"; -import { existsSync, mkdirSync, readdirSync, renameSync } from "fs"; -import { join, resolve } from "path"; +import { existsSync, mkdirSync, unlinkSync, renameSync } from "fs"; +import { resolve } from "path"; import tar from "tar"; export async function buildWebOn(args: { assetDir: string }) { checkDir(args.assetDir); - // Check if the assetDir path ends with '/out' const isOutDir = args.assetDir.endsWith("/out"); - const outDir = resolve(args.assetDir); - const outDirPath = resolve(args.assetDir, "..", "out"); - console.log("outDir: " + outDir.toString()); + const outDirPath = isOutDir ? args.assetDir : resolve(args.assetDir); + console.log("outDirPath: " + outDirPath.toString()); if (!isOutDir) { - console.log("outDirPath: " + outDirPath.toString()); - + console.log("Renaming asset directory to 'out'..."); try { - // Rename the assetDir to include '/out' renameSync(args.assetDir, outDirPath); - console.log("Renaming asset directory to 'out'..."); } catch (error) { console.error(`Error renaming directory: ${error}`); return; @@ -27,19 +22,17 @@ export async function buildWebOn(args: { assetDir: string }) { console.log("Directories are already named correctly, no need to rename."); } - // Create the "out" directory if it doesn't exist in the root path - if (!existsSync(outDir)) { + if (!existsSync(outDirPath)) { console.log("Creating 'out' directory..."); - mkdirSync(outDir); + mkdirSync(outDirPath); } - // Check if the "out" directory contains required files + const requiredFiles = [ "index.html", "nomo_icon.svg", "nomo_manifest.json", - ].map((file) => { - return join(outDirPath, file); - }); + ].map((file) => resolve(outDirPath, file)); + const missingFiles = requiredFiles.filter((file) => !existsSync(file)); if (missingFiles.length > 0) { @@ -51,18 +44,23 @@ export async function buildWebOn(args: { assetDir: string }) { return; } - // Create a tar.gz file const tarFileName = "nomo.tar.gz"; - const tarFilePath = joinDirWithFileName(outDir, tarFileName); - console.log(`Creating tar.gz file: ${getDebugPath(tarFilePath)}`); + const tarFilePath = joinDirWithFileName(outDirPath, tarFileName); + + if (existsSync(tarFilePath)) { + console.log(`Deleting existing ${tarFileName}...`); + unlinkSync(tarFilePath); + } + + console.log(`Creating new ${tarFileName}: ${tarFilePath}`); - // Use the tar.create method to properly await the completion await tar.create( { file: tarFilePath, gzip: true, + cwd: resolve(outDirPath, ".."), }, - [outDir] + ["out"] ); console.log("Build and packaging completed!"); @@ -75,5 +73,5 @@ function checkDir(dir: string): void { } function getDebugPath(path: string): string { - return `\'${resolve(path)}\'`; // Show an absolute path to users in case of errors. + return `\'${resolve(path)}\'`; } diff --git a/src/deploy-webon/deploy-webon.ts b/src/deploy-webon/deploy-webon.ts index 2c761fb..1904c28 100644 --- a/src/deploy-webon/deploy-webon.ts +++ b/src/deploy-webon/deploy-webon.ts @@ -1,4 +1,10 @@ -import { checkNotDir, logFatal } from "../util/util"; +import { + checkNotDir, + logFatal, + checkIfTarGz, + readCliConfig, +} from "../util/util"; +import { connectToSSH } from "../util/ssh-manager"; import { NomoManifest, NomoCliConfigs, GeneratedFile } from "../init/interface"; import { resolve } from "path"; @@ -8,6 +14,7 @@ export async function deployWebOn(args: { }) { const { deployTarget, archive } = args; checkNotDir(archive); + checkIfTarGz(archive); const nomoCliConfig = readCliConfig(); @@ -18,6 +25,12 @@ export async function deployWebOn(args: { const { rawSSH } = targetConfig; + try { + await connectToSSH({ deployTarget, archive }); + } catch (e) { + logFatal("Failed to connect to SSH"); + } + if ("sshPort" in rawSSH) { const { sshHost, sshBaseDir, publicBaseUrl, sshPort } = rawSSH; console.log(`Deploying to ${deployTarget}...`); @@ -36,15 +49,3 @@ export async function deployWebOn(args: { console.log(`Archive Path: ${archive}`); } } - -function readCliConfig(): NomoCliConfigs { - const cliPath = resolve("./nomo_cli.config.js"); - try { - // @ts-ignore - const nomoCliConfig = require(cliPath); - console.log(nomoCliConfig); - return nomoCliConfig; - } catch (e) { - logFatal("Could not find cli_config " + cliPath); - } -} diff --git a/src/init/init.ts b/src/init/init.ts index 9b3b34e..57a1c22 100644 --- a/src/init/init.ts +++ b/src/init/init.ts @@ -107,7 +107,7 @@ async function getValidWebOnId(prompt: string): Promise { while (!isValidWebOnId(webonId)) { console.error(`Invalid webon_id: ${webonId}`); webonId = await getUserInput( - "Enter an unique valid webon_id like demo.web.app for example:" + "Enter an unique valid webon_id like demo.web.app:" ); } return webonId; diff --git a/src/util/ssh-manager.ts b/src/util/ssh-manager.ts new file mode 100644 index 0000000..41e7ed0 --- /dev/null +++ b/src/util/ssh-manager.ts @@ -0,0 +1,36 @@ +import { logFatal, readCliConfig, runCommandsSequentially } from "../util/util"; + +let sshConnect = ""; + +export async function connectToSSH(args: { + deployTarget: string; + archive: string; +}) { + const { deployTarget, archive } = args; + + const nomoCliConfig = readCliConfig(); + + const targetConfig = nomoCliConfig.deployTargets[deployTarget]; + if (!targetConfig) { + logFatal(`Invalid deployTarget: ${deployTarget}`); + } + + const { rawSSH } = targetConfig; + + const { sshHost, sshBaseDir, publicBaseUrl, sshPort } = rawSSH; + const portOption = sshPort ? `-p ${sshPort}` : ""; + sshConnect = `ssh -t ${sshHost} ${portOption}`; + + const commands = [ls(), checkCreateDir(sshBaseDir)]; + + await runCommandsSequentially(commands); +} + +function ls(): string { + return `${sshConnect} 'ls'`; +} + +function checkCreateDir(sshBaseDir: string): string { + const mkdirCommand = `if [ ! -d ${sshBaseDir} ]; then mkdir -p ${sshBaseDir} && echo "Directory created"; else echo "Directory already exists"; fi`; + return `${sshConnect} "${mkdirCommand}"`; +} diff --git a/src/util/util.ts b/src/util/util.ts index bc99a30..ef8fd89 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,6 +1,8 @@ import { existsSync, lstatSync, unlinkSync } from "fs"; import { join, resolve } from "path"; import semver from "semver"; +import { NomoCliConfigs } from "../init/interface"; +import { exec } from "child_process"; let _isUnitTest: boolean = false; @@ -17,6 +19,19 @@ function isDirectory(path: string): boolean { return false; } } +export function readCliConfig(): NomoCliConfigs { + const cliPath = resolve("./nomo_cli.config.js"); + try { + // @ts-ignore + const nomoCliConfig = require(cliPath); + return nomoCliConfig; + } catch (e) { + logFatal( + "Could not find nomo_cli.config.js, run nomo-webon-cli init to create one. " + + cliPath + ); + } +} export function checkDir(dir: string): void { checkExists(dir); @@ -32,6 +47,11 @@ export function checkNotDir(path: string, hint?: { errorHint: string }): void { } } +export function checkIfTarGz(archive: string) { + if (!archive.endsWith(".tar.gz")) { + logFatal(`Invalid archive: ${archive}. It should end with ".tar.gz"`); + } +} function checkExists(path: string, hint?: { errorHint: string }): void { if (!existsSync(path)) { logFatal(`${getDebugPath(path)} does not exist.`); @@ -106,3 +126,27 @@ export function compareSemanticVersions(versionA: string, versionB: string) { return 0; // versions are equal } + +export function runCommand(cmd: string, pwd?: string): Promise { + console.log(`Run command \'${cmd}\'`); + return new Promise((resolve, reject) => { + exec(cmd, (error, stdout, stderr) => { + console.log(stdout); + if (error) { + console.error(stderr); + logFatal(`Failed to execute \'${cmd}\'. See the output above.`); + //reject(stdout + stderr); + } else { + resolve(stdout); + } + }); + }); +} + +export async function runCommandsSequentially( + commands: string[] +): Promise { + for (const command of commands) { + await runCommand(command); + } +}