Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b623e52
(feat): adding update command
lifeofpavs Mar 20, 2026
6c9d65e
Merge branch 'main' into pavs/update-command
lifeofpavs Mar 20, 2026
ce29700
refactor: simplify UpdateCommand — use GitHub API, dedupe buffer/chec…
lifeofpavs Mar 20, 2026
ee23d30
chore: remove explicit User-Agent headers from UpdateCommand
lifeofpavs Mar 20, 2026
83b97f0
refactor: inline getBinaryAssetName into updateBinary
lifeofpavs Mar 20, 2026
f7431b0
refactor: derive supported platforms from checksums.txt
lifeofpavs Mar 20, 2026
8cb95a9
chore: remove dev workflow references from update command and docs
lifeofpavs Mar 20, 2026
78dc09b
feat: auto-execute package manager updates instead of printing hints
lifeofpavs Mar 20, 2026
b99a54a
refactor: delegate binary updates to install.sh release asset
lifeofpavs Mar 23, 2026
cf28133
fix: use mkdtemp for secure temp file creation in updateBinary
lifeofpavs Mar 23, 2026
2dc272c
refactor: embed install.sh at build time instead of fetching it
lifeofpavs Mar 23, 2026
0ac1ae4
refactor: fetch install.sh from release instead of embedding it
lifeofpavs Mar 23, 2026
af9ce3b
refactor: simplify update command to delegate entirely to install.sh
lifeofpavs Mar 23, 2026
ccc0464
docs: update update.md to reflect install.sh delegation
lifeofpavs Mar 23, 2026
9f1eab3
nits
lifeofpavs Mar 23, 2026
3cfcae1
refactor: remove shasum fallback from install.sh
lifeofpavs Mar 23, 2026
9bac55c
fix: align update flow with canonical installer
lifeofpavs Mar 23, 2026
6b8e726
refactor: fetch installer from main
lifeofpavs Mar 23, 2026
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
49 changes: 49 additions & 0 deletions docs/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Update

Self-update the CLI to the latest version.

## Usage

```bash
# Update to the latest version
jup update

# Check for updates without installing
jup update --check
```

## How it works

1. Checks the latest release on GitHub
2. Compares with the installed version
3. If a newer version is available, fetches and runs `install.sh` which handles the update (Volta → npm → standalone binary fallback)
4. Outputs the result

## JSON output

```js
// Up to date
{
"currentVersion": "0.4.0",
"latestVersion": "0.4.0",
"status": "up_to_date"
}

// Update available (--check)
{
"currentVersion": "0.3.0",
"latestVersion": "0.4.0",
"status": "update_available"
}

// Updated
{
"currentVersion": "0.3.0",
"latestVersion": "0.4.0",
"status": "updated"
}
```

## Notes

- Supported platforms: `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`.
25 changes: 15 additions & 10 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ set -euo pipefail
PACKAGE="@jup-ag/cli"
BINARY="jup"
REPO="jup-ag/cli"

info() { printf '\033[1;34m%s\033[0m\n' "$*"; }
error() { printf '\033[1;31merror: %s\033[0m\n' "$*" >&2; exit 1; }

Expand Down Expand Up @@ -46,26 +45,32 @@ URL="https://github.com/${REPO}/releases/latest/download/${ASSET}"

CHECKSUM_URL="https://github.com/${REPO}/releases/latest/download/checksums.txt"

TMP_DIR=$(mktemp -d)
TMP_BINARY="${TMP_DIR}/${BINARY}"
TMP_CHECKSUMS="${TMP_DIR}/checksums.txt"
trap 'rm -rf "$TMP_DIR"' EXIT

info "Downloading $URL..."
curl -fsSL "$URL" -o "/tmp/${BINARY}"
curl -fsSL "$CHECKSUM_URL" -o "/tmp/checksums.txt"
curl -fsSL "$URL" -o "$TMP_BINARY"
curl -fsSL "$CHECKSUM_URL" -o "$TMP_CHECKSUMS"

info "Verifying checksum..."
EXPECTED=$(grep "$ASSET" /tmp/checksums.txt | awk '{print $1}')
ACTUAL=$(sha256sum "/tmp/${BINARY}" | awk '{print $1}')
EXPECTED=$(grep "$ASSET" "$TMP_CHECKSUMS" | awk '{print $1}') || true
if [ -z "$EXPECTED" ]; then
error "No checksum found for $ASSET in checksums.txt"
fi
ACTUAL=$(sha256sum "$TMP_BINARY" | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
rm -f "/tmp/${BINARY}" "/tmp/checksums.txt"
error "Checksum verification failed. Expected $EXPECTED, got $ACTUAL"
fi
rm -f /tmp/checksums.txt

chmod +x "/tmp/${BINARY}"
chmod +x "$TMP_BINARY"

if [ -w "$INSTALL_DIR" ]; then
mv "/tmp/${BINARY}" "${INSTALL_DIR}/${BINARY}"
mv "$TMP_BINARY" "${INSTALL_DIR}/${BINARY}"
else
info "Elevated permissions required to install to $INSTALL_DIR"
sudo mv "/tmp/${BINARY}" "${INSTALL_DIR}/${BINARY}"
sudo mv "$TMP_BINARY" "${INSTALL_DIR}/${BINARY}"
fi

info "Installed $BINARY to ${INSTALL_DIR}/${BINARY}"
126 changes: 126 additions & 0 deletions src/commands/UpdateCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { execFileSync } from "child_process";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";

import chalk from "chalk";
import ky from "ky";
import type { Command } from "commander";

import { version as currentVersion } from "../../package.json";
import { Output } from "../lib/Output.ts";

export class UpdateCommand {
public static register(program: Command): void {
program
.command("update")
.description("Update the CLI to the latest version")
.option("--check", "Check for updates without installing")
.action((opts: { check?: boolean }) => this.update(opts));
}

private static async update(opts: { check?: boolean }): Promise<void> {
const latestVersion = await this.getLatestVersion();
const isUpToDate = !this.isNewer(latestVersion, currentVersion);

if (isUpToDate) {
return this.output({
json: { currentVersion, latestVersion, status: "up_to_date" },
rows: [
{ label: "Version", value: `v${currentVersion}` },
{ label: "Status", value: chalk.green("Already up to date") },
],
});
}

if (opts.check) {
return this.output({
json: { currentVersion, latestVersion, status: "update_available" },
rows: [
{ label: "Current Version", value: `v${currentVersion}` },
{
label: "Latest Version",
value: chalk.green(`v${latestVersion}`),
},
{ label: "Status", value: "Update available" },
],
});
}

if (!Output.isJson()) {
console.log(`Updating to v${latestVersion}...`);
}

await this.runInstallScript();

this.output({
json: { currentVersion, latestVersion, status: "updated" },
rows: [
{ label: "Previous Version", value: `v${currentVersion}` },
{
label: "New Version",
value: chalk.green(`v${latestVersion}`),
},
{ label: "Status", value: chalk.green("Updated successfully") },
],
});
}

private static output(opts: {
json: Record<string, string>;
rows: { label: string; value: string }[];
}): void {
if (Output.isJson()) {
Output.json(opts.json);
} else {
Output.table({ type: "vertical", rows: opts.rows });
}
}

private static async getLatestVersion(): Promise<string> {
try {
const release = await ky
.get("https://api.github.com/repos/jup-ag/cli/releases/latest")
.json<{ tag_name: string }>();

return release.tag_name.replace(/^v/, "");
} catch {
throw new Error(
"Failed to check for updates. Please check your internet connection and try again."
);
}
}

private static isNewer(latest: string, current: string): boolean {
const latestParts = latest.split(".").map(Number);
const currentParts = current.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if ((latestParts[i] ?? 0) > (currentParts[i] ?? 0)) {
return true;
}
if ((latestParts[i] ?? 0) < (currentParts[i] ?? 0)) {
return false;
}
}
return false;
}

private static async runInstallScript(): Promise<void> {
const scriptUrl =
"https://raw.githubusercontent.com/jup-ag/cli/main/scripts/install.sh";
const dir = await mkdtemp(join(tmpdir(), "jup-"));
const scriptPath = join(dir, "install.sh");

try {
const script = await ky.get(scriptUrl).text();
await writeFile(scriptPath, script, { mode: 0o700 });
execFileSync("bash", [scriptPath], { stdio: "inherit" });
} catch {
throw new Error(
"Update failed. Run `jup update` again or install manually from https://github.com/jup-ag/cli/releases"
);
} finally {
rm(dir, { recursive: true, force: true }).catch(() => {});
}
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { KeysCommand } from "./commands/KeysCommand.ts";
import { LendCommand } from "./commands/LendCommand.ts";
import { PerpsCommand } from "./commands/PerpsCommand.ts";
import { SpotCommand } from "./commands/SpotCommand.ts";
import { UpdateCommand } from "./commands/UpdateCommand.ts";

import { version } from "../package.json";
import { Config } from "./lib/Config.ts";
Expand Down Expand Up @@ -34,6 +35,7 @@ KeysCommand.register(program);
LendCommand.register(program);
PerpsCommand.register(program);
SpotCommand.register(program);
UpdateCommand.register(program);

program.parseAsync().catch(async (err: unknown) => {
await Output.error(err);
Expand Down
Loading