diff --git a/README.md b/README.md index 629a122..f0ad10b 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,18 @@ writes from a terminal or automation workflow. ### Shell Script +> **Note:** The installer downloads a versioned GitHub Release asset and verifies +> its `.sha256` checksum before installing. +> +> On Windows, run this command inside +> [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) or Git Bash. + ```bash curl -fsSL https://account.megaeth.com/install | sh ``` -The installer downloads the latest release, verifies its checksum, installs the -`mega` command, and installs the bundled agent skill. Add the printed install -directory to `PATH` if needed. +The installer installs the `mega` command and the bundled agent skill. Add the +printed install directory to `PATH` if needed. Install a specific release: @@ -25,6 +30,9 @@ Install a specific release: curl -fsSL https://account.megaeth.com/install | sh -- --version v0.1.0 ``` +Prefer to inspect the artifact manually first? See the +[GitHub Releases page](https://github.com/megaeth-labs/wallet-cli/releases). + ### Build From Source ```bash @@ -37,6 +45,28 @@ pnpm build Requires Node.js 22 or newer and pnpm. +## Uninstall + +Remove the CLI, installed releases, and the wrapper script: + +```bash +curl -fsSL https://raw.githubusercontent.com/megaeth-labs/wallet-cli/main/scripts/uninstall.sh | sh +``` + +To also remove local wallet profiles and delegated key material: + +```bash +curl -fsSL https://raw.githubusercontent.com/megaeth-labs/wallet-cli/main/scripts/uninstall.sh | sh -- --config +``` + +If you installed from source: + +```bash +./scripts/uninstall.sh +# or with profile cleanup: +./scripts/uninstall.sh --config +``` + ## Quick Start ```bash diff --git a/scripts/install-release.sh b/scripts/install-release.sh index 231bf4a..c208152 100755 --- a/scripts/install-release.sh +++ b/scripts/install-release.sh @@ -173,7 +173,11 @@ check_prerequisites() { node_major="$(node -p 'Number(process.versions.node.split(".")[0])')" if [ "$node_major" -lt "$required_node_major" ]; then - error "Node.js >= $required_node_major is required; found major version $node_major" + error "Node.js >= $required_node_major is required, but you have Node.js $(node -v 2>/dev/null || echo unknown). + +To install Node.js 22: + • Using nvm: nvm install 22 + • Using apt: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs" fi } diff --git a/src/auth/loopback.ts b/src/auth/loopback.ts index a3c70b1..85a89ed 100644 --- a/src/auth/loopback.ts +++ b/src/auth/loopback.ts @@ -103,7 +103,7 @@ export type DelegatedKeyPair = { accessAddress: HexString; }; -export type BrowserOpener = (url: string) => Promise | void; +export type BrowserOpener = (url: string) => Promise | boolean | void; export type LoopbackLoginOptions = { network: Network; @@ -316,7 +316,18 @@ export async function runLoopbackLogin( waitForCallback.catch(() => undefined); try { - await (options.openBrowser ?? openSystemBrowser)(authUrl); + const browserOpened = await (options.openBrowser ?? openSystemBrowser)(authUrl); + if (browserOpened === false) { + process.stderr.write( + `⚠️ Could not open a browser automatically. +Open this URL in your browser to continue: + +${authUrl} + +Waiting for approval... +`, + ); + } const callback = await waitForCallback; if (callback.status !== "approved") { @@ -397,7 +408,18 @@ export async function authorizeLoopbackKey( waitForCallback.catch(() => undefined); try { - await (options.openBrowser ?? openSystemBrowser)(authUrl); + const browserOpened = await (options.openBrowser ?? openSystemBrowser)(authUrl); + if (browserOpened === false) { + process.stderr.write( + `⚠️ Could not open a browser automatically. +Open this URL in your browser to continue: + +${authUrl} + +Waiting for approval... +`, + ); + } const callback = await waitForCallback; if (callback.status !== "approved") { @@ -526,7 +548,18 @@ export async function runLoopbackRevoke( waitForCallback.catch(() => undefined); try { - await (options.openBrowser ?? openSystemBrowser)(authUrl); + const browserOpened = await (options.openBrowser ?? openSystemBrowser)(authUrl); + if (browserOpened === false) { + process.stderr.write( + `⚠️ Could not open a browser automatically. +Open this URL in your browser to continue: + +${authUrl} + +Waiting for approval... +`, + ); + } const callback = await waitForCallback; if (callback.status !== "approved") { @@ -908,27 +941,19 @@ export function keccak256(input: Uint8Array): Buffer { return output; } -export async function openSystemBrowser(url: string): Promise { +export async function openSystemBrowser(url: string): Promise { const { command, args } = browserCommand(url); const child = spawn(command, args, { - stdio: ["ignore", "ignore", "pipe"], + stdio: ["ignore", "ignore", "ignore"], windowsHide: true, }); - let stderr = ""; - child.stderr?.on("data", (chunk: Buffer) => { - stderr += chunk.toString("utf8"); + const result = await new Promise((resolve) => { + child.once("error", () => resolve(false)); + child.once("close", (code) => resolve(code === 0)); }); - const code = await new Promise((resolve, reject) => { - child.once("error", reject); - child.once("close", resolve); - }); - - if (code !== 0) { - const message = stderr.trim() || `${command} exited with status ${code}`; - throw new CliError(`failed to open browser: ${message}`); - } + return result; } function handleCallbackRequest( diff --git a/src/cli.ts b/src/cli.ts index 8263be9..1f8b5ed 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,5 @@ +import { createRequire } from "node:module"; + import { Command } from "commander"; import { registerWalletCommands } from "./commands/wallet.js"; @@ -5,13 +7,16 @@ import { formatErrorMessage } from "./errors.js"; export const commandName = "mega"; +const require = createRequire(import.meta.url); +const { version } = require("../package.json") as { version: string }; + export function createCli(): Command { const program = new Command(); program .name(commandName) .description("MegaETH MOSS account CLI") - .version("0.1.0") + .version(version) .showHelpAfterError() .exitOverride(); diff --git a/src/commands/wallet.test.ts b/src/commands/wallet.test.ts index 3dd6697..3d198a5 100644 --- a/src/commands/wallet.test.ts +++ b/src/commands/wallet.test.ts @@ -1103,6 +1103,67 @@ describe("wallet status commands", () => { expect(stdout.text).toContain("Network: mainnet"); }); + + it("falls back to a printed auth URL when the browser cannot be opened", async () => { + const env = await tempEnv(); + const stdout = memoryOutput(); + const stderr = memoryOutput({ columns: 80, isTTY: true }); + const program = new Command(); + program.exitOverride(); + registerWalletCommands(program, { + env, + now: () => activeNow, + openBrowser: async (url) => { + const authUrl = new URL(url); + const redirectUri = authUrl.searchParams.get("redirectUri"); + expect(redirectUri).not.toBeNull(); + setTimeout(async () => { + const callbackUrl = new URL(redirectUri!); + callbackUrl.searchParams.set("state", authUrl.searchParams.get("state")!); + callbackUrl.searchParams.set("status", "approved"); + callbackUrl.searchParams.set( + "accountAddress", + "0x1111111111111111111111111111111111111111", + ); + await fetch(callbackUrl); + }, 20); + return false; + }, + stderr, + stdout, + }); + + const originalWrite = process.stderr.write.bind(process.stderr); + let fallbackOutput = ""; + process.stderr.write = ((chunk: string | Uint8Array) => { + fallbackOutput += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + return true; + }) as typeof process.stderr.write; + + try { + await program.parseAsync([ + "node", + "mega", + "moss", + "login", + "--wallet-url", + "https://wallet.example", + "--wallet-api-url", + "https://wallet-api.example", + "--relay-url", + "https://relay.example", + ]); + } finally { + process.stderr.write = originalWrite; + } + + const plainStderr = stripAnsi(stderr.text + fallbackOutput); + expect(plainStderr).toContain("⚠️ Could not open a browser automatically."); + expect(plainStderr).toContain("Open this URL in your browser to continue:"); + expect(plainStderr).toContain("Waiting for approval..."); + expect(stdout.text).toContain("[ok] MOSS wallet connected"); + }); + it("prints the login intro and auth URL in no-browser mode", async () => { const env = await tempEnv(); const stdout = memoryOutput(); diff --git a/src/commands/wallet.ts b/src/commands/wallet.ts index 96be277..9051581 100644 --- a/src/commands/wallet.ts +++ b/src/commands/wallet.ts @@ -1459,7 +1459,21 @@ function makeBrowserOpener( return; } - await opener(url); + const opened = await opener(url); + if (opened === false) { + getStderr(dependencies).write( + `⚠️ Could not open a browser automatically. +Open this URL in your browser to continue: + +${url} + +Waiting for approval... +`, + ); + return false; + } + + return true; }; }