Skip to content
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
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,28 @@ 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:

```bash
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
Expand All @@ -37,6 +45,28 @@ pnpm build

Requires Node.js 22 or newer and pnpm.

## Uninstall

Remove the CLI, installed releases, and the wrapper script:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replace the existing uninstall guidance in # Logout and Uninstall with this? Also change the instructions to pipe to bash ( | bash)


```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
Expand Down
6 changes: 5 additions & 1 deletion scripts/install-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, can we make this cross-platform guidance?


  error "Node.js >= $required_node_major is required, but you have Node.js $(node -v 2>/dev/null || echo unknown).

  Install Node.js 22, then rerun the installer.

  Options:
    nvm:      nvm install 22
    Homebrew: brew install node
    Ubuntu:   curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"

fi
}

Expand Down
61 changes: 43 additions & 18 deletions src/auth/loopback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export type DelegatedKeyPair = {
accessAddress: HexString;
};

export type BrowserOpener = (url: string) => Promise<void> | void;
export type BrowserOpener = (url: string) => Promise<boolean | void> | boolean | void;

export type LoopbackLoginOptions = {
network: Network;
Expand Down Expand Up @@ -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...
`,
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my comment on the PR

const callback = await waitForCallback;

if (callback.status !== "approved") {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -908,27 +941,19 @@ export function keccak256(input: Uint8Array): Buffer {
return output;
}

export async function openSystemBrowser(url: string): Promise<void> {
export async function openSystemBrowser(url: string): Promise<boolean> {
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<boolean>((resolve) => {
child.once("error", () => resolve(false));
child.once("close", (code) => resolve(code === 0));
});

const code = await new Promise<number | null>((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(
Expand Down
7 changes: 6 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { createRequire } from "node:module";

import { Command } from "commander";

import { registerWalletCommands } from "./commands/wallet.js";
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();

Expand Down
61 changes: 61 additions & 0 deletions src/commands/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
16 changes: 15 additions & 1 deletion src/commands/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}

Expand Down