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
5 changes: 5 additions & 0 deletions .changeset/show-cli-update-notices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@alchemy/cli": patch
---

Show an upgrade notice when a newer CLI version is available during onboarding and interactive startup. Add an explicit `alchemy update-check` command so agents and scripts can retrieve current and latest version info with install guidance.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ alchemy

# Agent/script-friendly command
alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --json --no-interactive

# Agent checks whether a newer CLI version is available
alchemy update-check --json --no-interactive
```

#### Agent bootstrap
Expand All @@ -70,6 +73,8 @@ alchemy --json agent-prompt

This returns a single JSON document with execution policy, preflight instructions, auth matrix, the full command tree with all arguments and options, error codes with recovery actions, and example invocations. No external docs required.

Agents can also call `alchemy --json --no-interactive update-check` to retrieve the current CLI version, latest known version, and install command for upgrades.

## Command Reference

Run commands as `alchemy <command>`.
Expand Down Expand Up @@ -151,6 +156,7 @@ Use `alchemy help` or `alchemy help <command>` for generated command help.
| `apps origin-allowlist <id>` | Updates app origin allowlist | `alchemy apps origin-allowlist <app-id> --origins https://example.com` |
| `apps ip-allowlist <id>` | Updates app IP allowlist | `alchemy apps ip-allowlist <app-id> --ips 1.2.3.4,5.6.7.8` |
| `setup status` | Shows setup status + next commands | `alchemy setup status` |
| `update-check` | Checks whether a newer CLI version is available | `alchemy update-check --json --no-interactive` |
| `config set ...` | Sets config values | `alchemy config set api-key <key>` |
| `config get <key>` | Gets one config value | `alchemy config get network` |
| `config list` | Lists all config values | `alchemy config list` |
Expand Down Expand Up @@ -180,7 +186,7 @@ These apply to all commands.
|---|---|---|
| `--json` | — | Force JSON output |
| `-q, --quiet` | — | Suppress non-essential output |
| `-v, --verbose` | — | Enable verbose output |
| `--verbose` | — | Enable verbose output |
| `--no-color` | `NO_COLOR` | Disable color output |
| `--reveal` | — | Show secrets in plain text (TTY only) |

Expand Down Expand Up @@ -317,6 +323,7 @@ Use `--no-interactive` to disable REPL/prompts in automation.

- TTY: formatted human output
- Non-TTY: JSON output (script-friendly)
- `-v`, `--version`: prints the CLI version
- `--json`: forces JSON output in any context
- `--verbose` or `alchemy config set verbose true`: includes richer payload output on supported commands

Expand Down
2 changes: 2 additions & 0 deletions src/commands/agent-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ function buildAgentPrompt(program: Command): AgentPrompt {
"Parse stdout as JSON on exit code 0",
"Parse stderr as JSON on nonzero exit code",
"Never run bare 'alchemy' without --json --no-interactive",
"Run alchemy --json --no-interactive update-check when you need to detect available CLI upgrades",
],
preflight: {
command: "alchemy --json setup status",
Expand Down Expand Up @@ -191,6 +192,7 @@ function buildAgentPrompt(program: Command): AgentPrompt {
errors,
examples: [
"alchemy --json --no-interactive setup status",
"alchemy --json --no-interactive update-check",
"alchemy --json --no-interactive balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --api-key $ALCHEMY_API_KEY",
"alchemy --json --no-interactive apps list --access-key $ALCHEMY_ACCESS_KEY",
"alchemy --json --no-interactive rpc eth_blockNumber --api-key $ALCHEMY_API_KEY",
Expand Down
13 changes: 12 additions & 1 deletion src/commands/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getRPCNetworkIds } from "../lib/networks.js";
import { configDir, load as loadConfig } from "../lib/config.js";
import { getSetupMethod } from "../lib/onboarding.js";
import { bgRgb, rgb, noColor } from "../lib/colors.js";
import { getUpdateNoticeLines } from "../lib/update-check.js";

const COMMAND_NAMES = [
"apps",
Expand Down Expand Up @@ -81,6 +82,7 @@ const COMMAND_NAMES = [
"config get",
"config list",
"help",
"update-check",
"network",
"network list",
"nfts",
Expand Down Expand Up @@ -140,7 +142,10 @@ function saveReplHistory(lines: string[]): void {
writeFileSync(historyFilePath, normalized.join("\n") + "\n", { mode: 0o600 });
}

export async function startREPL(program: Command): Promise<void> {
export async function startREPL(
program: Command,
latestUpdate: string | null = null,
): Promise<void> {
if (!stdin.isTTY) return;
setReplMode(true);
setBrandedHelpSuppressed(true);
Expand Down Expand Up @@ -251,6 +256,12 @@ export async function startREPL(program: Command): Promise<void> {
console.log(` ${green("✓")} ${dim(`Configured auth: ${formatSetupMethodLabel()}`)}`);
console.log(` ${dim("Run commands directly (no 'alchemy' prefix).")}`);
console.log("");
if (latestUpdate) {
for (const line of getUpdateNoticeLines(latestUpdate)) {
console.log(line);
}
console.log("");
}
console.log(` ${brand("◆")} ${bold("Quick commands")}`);
console.log(` ${dim("- rpc eth_chainId")}`);
console.log(` ${dim("- config list")}`);
Expand Down
12 changes: 11 additions & 1 deletion src/commands/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
maskIf,
printKeyValueBox,
} from "../lib/ui.js";
import { getUpdateNoticeLines } from "../lib/update-check.js";
import { selectOrCreateApp } from "./config.js";
import { generateAndPersistWallet, importAndPersistWallet } from "./wallet.js";

Expand Down Expand Up @@ -115,7 +116,10 @@ async function runX402Onboarding(): Promise<void> {
console.log(` ${green("✓")} x402 enabled with wallet ${wallet.address}`);
}

export async function runOnboarding(_program: Command): Promise<boolean> {
export async function runOnboarding(
_program: Command,
latestUpdate: string | null = null,
): Promise<boolean> {
process.stdout.write(brandedHelp({ force: true }));
console.log("");
console.log(` ${brand("◆")} ${bold("Welcome to Alchemy CLI")}`);
Expand All @@ -124,6 +128,12 @@ export async function runOnboarding(_program: Command): Promise<boolean> {
console.log(` ${dim(" Choose one auth path to continue.")}`);
console.log(` ${dim(" Tip: select 'exit' to skip setup for now.")}`);
console.log("");
if (latestUpdate) {
for (const line of getUpdateNoticeLines(latestUpdate)) {
console.log(line);
}
console.log("");
}
const method = await promptSelect<OnboardingMethod>({
message: "Choose an auth setup path",
options: [
Expand Down
30 changes: 30 additions & 0 deletions src/commands/update-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Command } from "commander";
import { isJSONMode, printJSON } from "../lib/output.js";
import { dim, printKeyValueBox } from "../lib/ui.js";
import { getUpdateStatus } from "../lib/update-check.js";

function formatCheckedAt(checkedAt: number | null): string {
return checkedAt ? new Date(checkedAt).toISOString() : dim("(unknown)");
}

export function registerUpdateCheck(program: Command) {
program
.command("update-check")
.description("Check whether a newer CLI version is available")
.action(() => {
const status = getUpdateStatus();

if (isJSONMode()) {
printJSON(status);
return;
}

printKeyValueBox([
["Current version", status.currentVersion],
["Latest version", status.latestVersion ?? dim("(unknown)")],
["Update available", status.updateAvailable ? "yes" : "no"],
["Checked at", formatCheckedAt(status.checkedAt)],
["Install", status.installCommand],
]);
});
}
52 changes: 46 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command, Help } from "commander";
import { errSetupRequired, exitWithError, setReplMode } from "./lib/errors.js";
import { EXIT_CODES, errSetupRequired, exitWithError } from "./lib/errors.js";
import { setFlags, isJSONMode, quiet } from "./lib/output.js";
import { formatCommanderError } from "./lib/error-format.js";
import { load as loadConfig } from "./lib/config.js";
Expand Down Expand Up @@ -29,8 +29,10 @@ import { registerBundler } from "./commands/bundler.js";
import { registerGasManager } from "./commands/gas-manager.js";
import { registerSolana } from "./commands/solana.js";
import { registerAgentPrompt } from "./commands/agent-prompt.js";
import { registerUpdateCheck } from "./commands/update-check.js";
import { isInteractiveAllowed } from "./lib/interaction.js";
import { getSetupStatus, isSetupComplete, shouldRunOnboarding } from "./lib/onboarding.js";
import { getAvailableUpdate, printUpdateNotice } from "./lib/update-check.js";

// ── ANSI helpers for help formatting ────────────────────────────────
const hBrand = noColor
Expand Down Expand Up @@ -72,7 +74,7 @@ const ROOT_COMMAND_PILLARS = [
},
{
label: "Admin",
commands: ["apps", "config", "setup", "agent-prompt", "version", "help"],
commands: ["apps", "config", "setup", "agent-prompt", "update-check", "version", "help"],
},
] as const;

Expand Down Expand Up @@ -115,12 +117,27 @@ const findCommandByPath = (root: Command, path: string[]): Command | null => {

declare const __CLI_VERSION__: string;

let cachedAvailableUpdate: string | null | undefined;
let updateShownDuringInteractiveStartup = false;

function getAvailableUpdateOnce(): string | null {
if (cachedAvailableUpdate === undefined) {
cachedAvailableUpdate = getAvailableUpdate();
}
return cachedAvailableUpdate;
}

function resetUpdateNoticeState(): void {
cachedAvailableUpdate = undefined;
updateShownDuringInteractiveStartup = false;
}

program
.name("alchemy")
.description(
"The Alchemy CLI lets you query blockchain data, call JSON-RPC methods, and manage your Alchemy configuration.",
)
.version(__CLI_VERSION__)
.version(__CLI_VERSION__, "-v, --version", "display CLI version")
.option("--api-key <key>", "Alchemy API key (env: ALCHEMY_API_KEY)")
.option("--access-key <key>", "Alchemy access key (env: ALCHEMY_ACCESS_KEY)")
.option(
Expand All @@ -131,13 +148,23 @@ program
.option("--wallet-key-file <path>", "Path to wallet private key file for x402")
.option("--json", "Force JSON output")
.option("-q, --quiet", "Suppress non-essential output")
.option("-v, --verbose", "Enable verbose output")
.option("--verbose", "Enable verbose output")
.option("--no-color", "Disable color output")
.option("--reveal", "Show secrets in plain text (TTY only)")
.option("--timeout <ms>", "Request timeout in milliseconds", parseInt)
.option("--debug", "Enable debug diagnostics")
.option("--no-interactive", "Disable REPL and prompt-driven interactions")
.addHelpCommand(false)
.exitOverride((err) => {
if (
err.code === "commander.help" ||
err.code === "commander.helpDisplayed" ||
err.code === "commander.version"
) {
process.exit(0);
}
process.exit(EXIT_CODES.INVALID_ARGS);
})
.configureOutput({
outputError(str, write) {
write(formatCommanderError(str));
Expand Down Expand Up @@ -301,7 +328,12 @@ program
.hook("postAction", () => {
if (!isJSONMode() && !quiet) {
console.log("");
if (!updateShownDuringInteractiveStartup) {
const latest = getAvailableUpdateOnce();
if (latest) printUpdateNotice(latest);
}
}
resetUpdateNoticeState();
})
.action(async () => {
const cfg = loadConfig();
Expand All @@ -310,22 +342,29 @@ program
}

if (isInteractiveAllowed(program)) {
let latestForInteractiveStartup: string | null = null;
if (shouldRunOnboarding(program, cfg)) {
const { runOnboarding } = await import("./commands/onboarding.js");
const completed = await runOnboarding(program);
const latest = getAvailableUpdateOnce();
const completed = await runOnboarding(program, latest);
updateShownDuringInteractiveStartup = Boolean(latest);
latestForInteractiveStartup = null;
if (!completed) {
// User skipped or aborted onboarding while setup remains incomplete.
// Do not enter REPL; return to shell without forcing interactive mode.
return;
}
} else {
latestForInteractiveStartup = getAvailableUpdateOnce();
updateShownDuringInteractiveStartup = Boolean(latestForInteractiveStartup);
}
const { startREPL } = await import("./commands/interactive.js");
// In REPL mode, override exitOverride so errors don't kill the process
program.exitOverride();
program.configureOutput({
writeErr: () => {},
});
await startREPL(program);
await startREPL(program, latestForInteractiveStartup);
return;
}
program.help();
Expand Down Expand Up @@ -363,6 +402,7 @@ registerSetup(program);
registerConfig(program);
registerSolana(program);
registerAgentPrompt(program);
registerUpdateCheck(program);
registerVersion(program);
program
.command("help [command...]")
Expand Down
Loading
Loading