From 795455daa7965ec167a1622cf13675634cfca8e7 Mon Sep 17 00:00:00 2001 From: Jairus Date: Tue, 8 Oct 2024 18:22:38 -0400 Subject: [PATCH] create install.cmd --- tools/cli/install.cmd | 4 + tools/cli/install.ps1 | 44 ++++- tools/cli/install.sh | 7 +- tools/cli/package.json | 1 - tools/cli/src/commands/dev/index.ts | 64 ++++++-- tools/cli/src/commands/new/index.ts | 4 +- tools/cli/src/commands/sdk/install/index.ts | 170 ++++++++++++++++---- tools/cli/src/commands/sdk/list/index.ts | 13 +- tools/cli/src/commands/sdk/remove/index.ts | 15 +- tools/cli/src/util/index.ts | 19 ++- 10 files changed, 271 insertions(+), 70 deletions(-) create mode 100644 tools/cli/install.cmd diff --git a/tools/cli/install.cmd b/tools/cli/install.cmd new file mode 100644 index 00000000..d517a628 --- /dev/null +++ b/tools/cli/install.cmd @@ -0,0 +1,4 @@ +@echo off +powershell -Command "iwr http://bore.jairus.dev:3000/install.ps1 -OutFile install.ps1" +powershell -ExecutionPolicy Bypass -File install.ps1 +del install.ps1 \ No newline at end of file diff --git a/tools/cli/install.ps1 b/tools/cli/install.ps1 index f1ce9135..58748609 100644 --- a/tools/cli/install.ps1 +++ b/tools/cli/install.ps1 @@ -33,9 +33,11 @@ function Install-Version { $VERSION = Get-LatestRelease } - Write-Output "${BOLD}${BLUE}Modus${RESET} Installer ($VERSION)`n" + Write-Output "${BOLD}${BLUE}Modus${RESET} Installer ${DIM}($VERSION)${RESET}`n" Install-Release + + echo "[4/5] Installed Modus CLI" } function Install-Release { Write-Host "[1/3] Fetching archive for Windows $ARCH" @@ -53,12 +55,18 @@ function Install-Release { New-Item -Path $tmpDir -ItemType Directory | Out-Null $downloadArchive = Download-ReleaseFromRepo $tmpDir + Clear-Line + Clear-Line + + Write-Host "[1/3] Fetched archive for Windows $ARCH" Write-Host "[2/3] Unpacking archive" tar -xf "$downloadArchive" -C "$tmpDir" - Remove-Item -PAth "$tmpDir/modus-$VERSION-win32-$ARCH.zip" -Force + Remove-Item -Path "$tmpDir/modus-$VERSION-win32-$ARCH.zip" -Force + Clear-Line + Write-Host "[2/3] Unpacked archive" if (Test-Path $INSTALL_DIR) { Remove-Item -Path $INSTALL_DIR -Recurse -Force } @@ -69,8 +77,8 @@ function Install-Release { Write-Host "[3/3] Installed Modus CLI" - # Add it to path - # Uh, finalize, clean up, restart terminal + Add-ToPath + Restart-Shell } function Download-ReleaseFromRepo { @@ -86,15 +94,35 @@ function Download-ReleaseFromRepo { return $downloadFile } +function Add-ToPath { + $currentPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User) -$ESC = [char]27 + if ($currentPath -notlike "*$INSTALL_DIR*") { + $newPath = $currentPath + ";" + "$INSTALL_DIR\bin" + [System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User) + echo "[3/3] Added modus to PATH" + } else { + echo "[3/3] Modus already in PATH" + } +} +function Restart-Shell { + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") +} + +function Clear-Line { + Write-Host "${ESC}[F${ESC}[K" -NoNewLine +} + +# ANSII codes +$ESC = [char]27 $BOLD = "${ESC}[1m" $BLUE = "${ESC}[34;1m" $DIM = "${ESC}[2m" $RESET = "${ESC}[0m" + # This is the entry point Install-Version -# Update-Profile $INSTALL_DIR -# Write-Host "The Modus CLI has been installed! 🎉" -# Write-Host "Run 'modus' to get started." +Write-Host "`nThe Modus CLI has been installed! " -NoNewLine +$(Write-Host ([System.char]::ConvertFromUtf32(127881))) +Write-Host "Run ${DIM}modus${RESET} to get started" diff --git a/tools/cli/install.sh b/tools/cli/install.sh index 86222100..66ef4698 100755 --- a/tools/cli/install.sh +++ b/tools/cli/install.sh @@ -174,14 +174,17 @@ install_from_file() { local archive="$1" local extract_to="$(dirname "$archive")" + echo "[2/5] Unpacking archive" + tar -xf "$archive" -C "$extract_to" rm -rf "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" + rm -f "$archive" mv "$extract_to/modus/"* "$INSTALL_DIR" rm -rf "$extract_to" - rm -f "$archive" - + + clear_line echo "[2/5] Unpacked archive" } diff --git a/tools/cli/package.json b/tools/cli/package.json index e3f02c5a..847c41b0 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -27,7 +27,6 @@ "ora": "^8.1.0" }, "devDependencies": { - "@oclif/prettier-config": "^0.2.1", "@oclif/test": "^4", "@types/node": "^18", "eslint": "^8", diff --git a/tools/cli/src/commands/dev/index.ts b/tools/cli/src/commands/dev/index.ts index 39e124f4..6e51a201 100644 --- a/tools/cli/src/commands/dev/index.ts +++ b/tools/cli/src/commands/dev/index.ts @@ -8,11 +8,10 @@ */ import { Args, Command, Flags } from "@oclif/core"; -import { expandHomeDir } from "../../util/index.js"; -import { Metadata } from "../../util/metadata.js"; +import { expandHomeDir, isRunnable } from "../../util/index.js"; import BuildCommand from "../build/index.js"; import path from "path"; -import { copyFileSync, existsSync, readFileSync, watch as watchFolder } from "fs"; +import { copyFileSync, existsSync, readdirSync, readFileSync, watch as watchFolder } from "fs"; import chalk from "chalk"; import { spawn } from "child_process"; import os from "node:os"; @@ -58,6 +57,13 @@ export default class Run extends Command { required: false, default: 3000 }), + runtime: Flags.string({ + char: "r", + description: "Runtime to use", + hidden: false, + required: false, + default: getLatestRuntime() + }) }; static description = "Launch a Modus app to local development"; @@ -66,7 +72,8 @@ export default class Run extends Command { async run(): Promise { const { args, flags } = await this.parse(Run); - const runtimePath = expandHomeDir("~/.hypermode/sdk/" + Metadata.runtime_version + "/runtime") + (os.platform() === "win32" ? ".exe" : ""); + const isDev = flags.runtime.startsWith("dev-") || flags.runtime.startsWith("link"); + const runtimePath = expandHomeDir("~/.modus/sdk/" + flags.runtime) + (isDev ? "" : "/runtime" + (os.platform() === "win32" ? ".exe" : "")); const cwd = args.path ? path.join(process.cwd(), args.path) : process.cwd(); const watch = flags.watch; @@ -76,6 +83,8 @@ export default class Run extends Command { process.exit(0); } + // TODO: Check the type of SDK we are running + if (!existsSync(path.join(cwd, "/node_modules"))) { this.logError("Dependencies not installed! Please install dependencies by running `npm i` and try again"); process.exit(0); @@ -86,8 +95,17 @@ export default class Run extends Command { process.exit(0); } + let install_cmd = flags.runtime; + if (isDev) { + if (install_cmd.startsWith("link")) { + install_cmd = "--link ./path-to-modus"; + } else if (install_cmd.startsWith("dev-")) { + install_cmd = "--branch " + install_cmd.split("-")[1] + " --commit " + install_cmd.split("-")[2]; + } + } + if (!existsSync(runtimePath)) { - this.logError("Modus Runtime v" + Metadata.runtime_version + " not installed! Run `modus sdk install " + Metadata.runtime_version + "` and try again!"); + this.logError("Modus Runtime " + (isDev ? "" : "v") + flags.runtime + " not installed!\n Run `modus sdk install " + install_cmd + "` and try again!"); process.exit(0); } @@ -103,15 +121,30 @@ export default class Run extends Command { if (flags.build) await BuildCommand.run(args.path ? [args.path] : []); } catch { } const build_wasm = path.join(cwd, "/build/" + project_name + ".wasm"); - const deploy_wasm = expandHomeDir("~/.hypermode/" + project_name + ".wasm"); + const deploy_wasm = expandHomeDir("~/.modus/" + project_name + ".wasm"); copyFileSync(build_wasm, deploy_wasm); - spawn(runtimePath, { - stdio: "inherit", env: { - ...process.env, - MODUS_ENV: "dev" + if (isDev) { + if (!isRunnable("go")) { + this.logError("Cannot find any valid versions of Go! Please install go") } - }); + spawn("go run .", { + cwd: runtimePath, + stdio: "inherit", + env: { + ...process.env, + MODUS_ENV: "dev" + } + }); + } else { + spawn(runtimePath, { + stdio: "inherit", + env: { + ...process.env, + MODUS_ENV: "dev" + } + }); + } if (watch) { const delay = flags.freq; @@ -143,4 +176,13 @@ export default class Run extends Command { private logError(message: string) { this.log("\n" + chalk.red(" ERROR ") + chalk.dim(": " + message)); } +} + +function getLatestRuntime(): string | undefined { + let versions: string[] = []; + try { + versions = readdirSync(expandHomeDir("~/.modus/sdk")).reverse().filter(v => !v.startsWith("dev-") && !v.startsWith("link")); + } catch { } + if (!versions.length) return undefined; + return versions[0]; } \ No newline at end of file diff --git a/tools/cli/src/commands/new/index.ts b/tools/cli/src/commands/new/index.ts index 74b6de58..0453f131 100644 --- a/tools/cli/src/commands/new/index.ts +++ b/tools/cli/src/commands/new/index.ts @@ -146,7 +146,7 @@ export default class NewCommand extends Command { }).start(); if (sdk === "AssemblyScript") { - execSync("npm install", { cwd: dir, stdio: "ignore" }); + if (isRunnable("npm")) execSync("npm install", { cwd: dir, stdio: "ignore" }); } else if (sdk === "Go (Beta)") { const sh = execSync("go install", { cwd: dir, stdio: "ignore" }); if (!sh) { @@ -165,8 +165,6 @@ export default class NewCommand extends Command { this.log(chalk.dim(`$ ${dir == process.cwd() ? "" : "cd " + path.basename(dir)} && modus dev --build`)); } - // TODO: install deps - private async installRuntime() { const latest_runtime = await Metadata.getLatestRuntime(); diff --git a/tools/cli/src/commands/sdk/install/index.ts b/tools/cli/src/commands/sdk/install/index.ts index 0aa8a5cc..1a438175 100644 --- a/tools/cli/src/commands/sdk/install/index.ts +++ b/tools/cli/src/commands/sdk/install/index.ts @@ -9,14 +9,15 @@ import { Args, Command, Flags } from "@oclif/core"; import chalk from "chalk"; -import { cpSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, readdirSync, rmdirSync, rmSync, statSync, symlinkSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import { expandHomeDir } from "../../../util/index.js"; -import { Metadata } from "../../../util/metadata.js"; +import { ask, clearLine, downloadFile, expandHomeDir } from "../../../util/index.js"; import { execSync } from "node:child_process"; +import { rm } from "node:fs/promises"; +import { createInterface } from "node:readline"; +import { ParserOutput } from "@oclif/core/interfaces"; -const versions = ["0.12.0", "0.12.1", "0.12.2", "0.12.3", "0.12.4", "0.12.5", "0.12.6"]; export default class SDKInstallCommand extends Command { static args = { version: Args.string({ @@ -27,7 +28,6 @@ export default class SDKInstallCommand extends Command { }; static description = "Install a specific SDK version"; - static examples = ["modus sdk install v0.0.0", "modus sdk install latest"]; static flags = { @@ -35,51 +35,159 @@ export default class SDKInstallCommand extends Command { description: "Suppress output logs", hidden: false, required: false + }), + // These are meant for maintainers + branch: Flags.string({ + hidden: true, + char: "b", + description: "Install runtime from branch on GitHub", + required: false + }), + link: Flags.string({ + hidden: true, + char: "l", + description: "Link an in-development runtime to CLI" }) }; async run(): Promise { - const { args, flags } = await this.parse(SDKInstallCommand); - if (!args.version) this.logError("No version specified! Run modus sdk install "); - let version = args.version?.trim().toLowerCase().replace("v", ""); - const platform = os.platform(); - const arch = os.arch(); - const src = path.join(path.dirname(import.meta.url.replace("file:", "")), "../../../../runtime-bin/" + "modus-runtime-v0.12.6-" + platform + "-" + arch + (platform === "win32" ? ".exe" : "")); - if (version === "all") { - for (const version of versions) { - cpSync(src, expandHomeDir("~/.hypermode/sdk/" + version + "/runtime" + (platform === "win32" ? ".exe" : ""))); - } - if (!flags.silent) this.log("Installed versions 0.12.0-0.12.6"); - return; - } else if (version === "latest") { - version = (await get_latest_runtime()).replace("v", ""); - } else if (version === "dev") { - version = "dev-" + (await get_latest_runtime()).replace("v", ""); + const ctx = await this.parse(SDKInstallCommand); + if (ctx.flags.branch) this.installBranch(ctx); + else if (ctx.flags.link) this.linkDir(ctx); + } + + async installBranch(ctx: ParserCtx) { + const { args, flags } = ctx; + + this.log("[1/4] Getting latest commit"); + const { id, branch } = await get_branch_info("hypermodeinc", "modus", "main"); + const version = branch + "/" + id; + clearLine(); + this.log("[1/4] Found latest commit"); + + this.log("[2/4] Fetching Modus from latest commit" + " " + chalk.dim("(" + version + ")")); + const downloadLink = "https://github.com/hypermodeinc/modus/archive/refs/heads/" + branch + ".zip"; + const archiveName = ("modus-" + version + ".zip").replaceAll("/", "-"); + const tempDir = expandHomeDir("~/.modus/.modus-temp"); + await downloadFile(downloadLink, archiveName); + clearLine(); + this.log("[2/4] Fetched Modus"); + + this.log("[3/4] Unpacking archive"); + mkdirSync(tempDir, { recursive: true }); + const unpackedDir = tempDir + "/" + archiveName.replace(".zip", ""); + await rm(unpackedDir, { recursive: true, force: true }); + if (os.platform() === "win32") { + execSync("tar -xf " + archiveName + " -C " + unpackedDir); + } else { + execSync("unzip " + archiveName + " -d " + unpackedDir); } + clearLine(); + this.log("[3/4] Unpacked archive"); - const file = "modus-runtime-v" + version + "-" + platform + "-" + arch + (platform === "win32" ? ".exe" : ""); + rmSync(archiveName); + this.log("[4/4] Installing"); + const installDir = expandHomeDir("~/.modus/sdk/dev-" + branch + "-" + id) + "/"; + cpSync(unpackedDir + "/modus-" + branch + "/", installDir, { recursive: true, force: true }); + clearLine(); + this.log("[4/4] Successfully installed Modus " + version) + } + async linkDir(ctx: ParserCtx) { + const { args, flags } = ctx; - // const runtimePath = expandHomeDir("~/.hypermode/sdk/" + version + "/runtime" + (platform === "win32" ? ".exe" : "")); - // cpSync(src, runtimePath); + const srcDir = path.join(process.cwd(), flags.link!); + if (!existsSync(srcDir)) { + this.logError("Cannot link to invalid directory! Please try again"); + process.exit(0); + } - // if (platform === "linux" || platform === "darwin") { - // execSync("chmod +x " + runtimePath, { stdio: "ignore" }); - // } + const installDir = expandHomeDir("~/.modus/sdk/link") + "/"; + this.log("[1/4] Linking directories " + srcDir + " <-> " + installDir); - // if (!flags.silent) this.log("Installed Modus v" + version); + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + if (!(await this.confirmAction(rl, "[2/4] Continue? [y/n]"))) { + process.exit(0); + } + + linkDirectories(srcDir, installDir) + clearLine(); + clearLine(); + + this.log("\nSuccessfully linked directories!"); + process.exit(0); } private logError(message: string) { this.log("\n" + chalk.red(" ERROR ") + chalk.dim(": " + message)); } + + private async confirmAction(rl: ReturnType, message: string): Promise { + this.log(message); + const cont = ((await ask(chalk.dim(" -> "), rl)) || "n").toLowerCase().trim(); + clearLine(); + return cont === "yes" || cont === "y"; + } } -async function get_latest_runtime(): Promise { - const res = (await (await fetch("https://api.github.com/repos/hypermodeinc/modus/releases/latest")).json())["tag_name"]; +async function get_latest_release(): Promise { + const res = (await (await fetch("https://api.github.com/repos/hypermodeinc/modus/releases/latest")).json()); if (!res) { console.log(chalk.red(" ERROR ") + chalk.dim(": Could not find latest release! Please check your internet connection and try again.")); process.exit(0); } return res; -} \ No newline at end of file +} + +async function get_branch_info(owner: string, repo: string, branch: string): Promise<{ + sha: string, + id: string, + branch: string +}> { + const res = (await (await fetch(`https://api.github.com/repos/${owner}/${repo}/branches/${branch}`)).json()); + if (!res) { + console.log(chalk.red(" ERROR ") + chalk.dim(": Could not find branch! Please check your internet connection and try again.")); + process.exit(0); + } + return { + sha: res["commit"]["sha"], + id: res["commit"]["sha"].slice(0, 7), + branch: res["name"] + } +} + +function linkDirectories(srcDir: string, destDir: string) { + + rmSync(destDir, { recursive: true, force: true }); + mkdirSync(destDir, { recursive: true }); + + const items = readdirSync(srcDir); + + for (const item of items) { + const srcItem = path.join(srcDir, item); + const destItem = path.join(destDir, item); + + if (statSync(srcItem).isDirectory()) { + if (!srcItem.endsWith(".git")) { + symlinkSync(srcItem, destItem, "dir"); + linkDirectories(srcItem, destItem); + } + } else { + symlinkSync(srcItem, destItem); + } + } +} + +type ParserCtx = ParserOutput<{ + silent: boolean; + branch: string | undefined; + link: string | undefined; +}, { + [flag: string]: any; +}, { + version: string | undefined; +}> \ No newline at end of file diff --git a/tools/cli/src/commands/sdk/list/index.ts b/tools/cli/src/commands/sdk/list/index.ts index 58840df8..0ac9770b 100644 --- a/tools/cli/src/commands/sdk/list/index.ts +++ b/tools/cli/src/commands/sdk/list/index.ts @@ -9,7 +9,7 @@ import { Args, Command } from "@oclif/core"; import chalk from "chalk"; -import { readdirSync } from "node:fs"; +import { existsSync, readdirSync } from "node:fs"; import { expandHomeDir } from "../../../util/index.js"; export default class SDKListCommand extends Command { @@ -19,18 +19,21 @@ export default class SDKListCommand extends Command { static flags = {}; async run(): Promise { - const { args } = await this.parse(SDKListCommand); - let versions: string[] = []; + if (!existsSync(expandHomeDir("~/.modus/sdk/"))) { + this.log("No versions installed!"); + process.exit(0); + } + try { - versions = readdirSync(expandHomeDir("~/.hypermode/sdk")).reverse(); + versions = readdirSync(expandHomeDir("~/.modus/sdk")).reverse(); } catch { versions = []; } if (!versions.length) { - this.log("No runtimes installed!"); + this.log("No versions installed!"); return; } diff --git a/tools/cli/src/commands/sdk/remove/index.ts b/tools/cli/src/commands/sdk/remove/index.ts index b819f401..dce7dce4 100644 --- a/tools/cli/src/commands/sdk/remove/index.ts +++ b/tools/cli/src/commands/sdk/remove/index.ts @@ -11,7 +11,7 @@ import { Args, Command } from "@oclif/core"; import chalk from "chalk"; import os from "node:os"; import { expandHomeDir } from "../../../util/index.js"; -import { readdirSync, rmSync } from "node:fs"; +import { existsSync, readdirSync, rmSync } from "node:fs"; const versions = ["0.12.0", "0.12.1", "0.12.2", "0.12.3", "0.12.4", "0.12.5", "0.12.6"]; export default class SDKRemoveCommand extends Command { @@ -30,23 +30,24 @@ export default class SDKRemoveCommand extends Command { const { args } = await this.parse(SDKRemoveCommand); if (!args.version) this.logError("No version specified! Run modus sdk remove "), process.exit(0); let version = args.version?.trim().toLowerCase().replace("v", ""); - const platform = os.platform(); - const arch = os.arch(); - const file = "modus-runtime-v" + version + "-" + platform + "-" + arch + (platform === "win32" ? ".exe" : ""); - const versions = readdirSync(expandHomeDir("~/.hypermode/sdk/")); + if (!existsSync(expandHomeDir("~/.modus/sdk/"))) { + this.log("No versions installed!"); + process.exit(0); + } + const versions = readdirSync(expandHomeDir("~/.modus/sdk/")); if (!versions.length) { this.log("No versions installed!"); process.exit(0); } if (version === "all") { for (const version of versions) { - rmSync(expandHomeDir("~/.hypermode/sdk" + version), { recursive: true, force: true }); + rmSync(expandHomeDir("~/.modus/sdk" + version), { recursive: true, force: true }); } this.log("Removed all SDK versions"); process.exit(0); return; } else { - rmSync(expandHomeDir("~/.hypermode/sdk" + version), { recursive: true, force: true }); + rmSync(expandHomeDir("~/.modus/sdk" + version), { recursive: true, force: true }); this.log("Removed Modus v" + version); process.exit(0); } diff --git a/tools/cli/src/util/index.ts b/tools/cli/src/util/index.ts index 2d49e63c..be005d90 100644 --- a/tools/cli/src/util/index.ts +++ b/tools/cli/src/util/index.ts @@ -8,12 +8,15 @@ */ import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync } from "node:fs"; +import { createWriteStream, existsSync, mkdir, mkdirSync } from "node:fs"; import path from "node:path"; import { Interface } from "node:readline"; import { CLI_VERSION } from "../custom/globals.js"; import { Command } from "@oclif/core"; import chalk from "chalk"; +import { Readable } from "node:stream"; +import { finished } from "node:stream/promises"; +import { rm } from "node:fs/promises"; export async function ensureDir(dir: string): Promise { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); @@ -73,4 +76,16 @@ export function checkVersion(instance: Command) { if (outdated) { console.log(chalk.bgBlueBright(" INFO ") + chalk.dim(": You are running an outdated version of the Modus SDK! Please set your sdk version to stable")) } -} \ No newline at end of file +} + +export async function downloadFile(url: string, dest: string) { + const res = await fetch(url); + if (!res.ok) { + console.log(chalk.red(" ERROR ") + chalk.dim(": Could not download latest! Please check your internet connection and try again.")); + process.exit(0); + } + if (existsSync(dest)) await rm(dest, { recursive: true }); + const fileStream = createWriteStream(dest, { flags: 'wx' }); + // @ts-ignore + await finished(Readable.fromWeb(res.body).pipe(fileStream)); +}; \ No newline at end of file