Skip to content

(feat): adding update command#9

Merged
AaronCQL merged 18 commits intomainfrom
pavs/update-command
Mar 24, 2026
Merged

(feat): adding update command#9
AaronCQL merged 18 commits intomainfrom
pavs/update-command

Conversation

@lifeofpavs
Copy link
Collaborator

@lifeofpavs lifeofpavs commented Mar 20, 2026

Summary

Adds jup update — a self-update command that fetches and runs install.sh from the matching release tag, which handles volta/npm/binary detection and installation internally.

Update command

  • jup update — fetches install.sh from the release tag URL, writes it to a secure temp directory (mkdtemp), and executes it
  • jup update --check — reports whether an update is available without installing
  • Version detection via GitHub REST API (/repos/jup-ag/cli/releases/latest)
  • Supports both JSON and table output formats

install.sh hardening

1. Secure temp file creation

The original script wrote to a predictable path (/tmp/jup), which is vulnerable to symlink attacks — an attacker could place a symlink at /tmp/jup pointing to another file, and the script would overwrite it. The fix uses mktemp -d for a unique temp directory and trap for cleanup:

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

2. Checksum grep error handling

Under set -e, if grep finds no matching checksum line it returns exit code 1, killing the script with no useful message. The fix suppresses the exit and provides an actionable error:

EXPECTED=$(grep "$ASSET" "$TMP_CHECKSUMS" | awk '{print $1}') || true
if [ -z "$EXPECTED" ]; then
  error "No checksum found for $ASSET in checksums.txt"
fi

…ksum parsing

- Switch getLatestVersion to GitHub REST API JSON response instead of redirect parsing
- Replace identity maps in getBinaryAssetName with Set-based validation
- Parse checksum line once and reuse; compute Buffer.from(binary) once
ky sends the runtime's default User-Agent automatically; GitHub only
rejects requests with an empty header, not a missing one.
Single-use method with no reuse benefit; inlining reduces indirection.
Fetch checksums before the binary to validate platform support
dynamically instead of hardcoding allowed platforms/archs.
Both npm and binary paths now run automatically. Removes the
manual_update_required status — all updates return status: updated.
@lifeofpavs lifeofpavs marked this pull request as ready for review March 20, 2026 17:33
@lifeofpavs lifeofpavs requested a review from AaronCQL March 20, 2026 17:33
Ship install.sh as a GitHub release asset and fetch it from the
release URL instead of raw.githubusercontent.com, so the update
command uses the same versioned script that users install with.

try {
const script = await ky.get(scriptUrl).text();
await writeFile(scriptPath, script);

Check failure

Code scanning / CodeQL

Insecure temporary file High

Insecure creation of file in the os temp dir.

Copilot Autofix

AI 2 days ago

In general, to fix insecure temporary file creation you should avoid constructing temp paths manually with os.tmpdir() and a static or predictable filename. Instead, use a library or API that creates the file atomically with secure permissions and a unique path, and then use that path. For Node.js, the tmp package is a well-known option that provides file() / fileSync() helpers that do this securely.

For this file, the best minimal fix is:

  • Import the tmp library.
  • In updateBinary, replace const scriptPath = join(tmpdir(), "jup-install.sh"); with creation of a secure temp file via tmp.
  • Use the returned name (path) to write and execute the script.
  • Keep the existing try/finally cleanup, but let tmp also track the file; rm remains harmless redundancy and preserves current behavior.

Concretely, in src/commands/UpdateCommand.ts:

  1. Add import tmp from "tmp"; near the other imports.
  2. Replace the scriptPath line with:
    const tmpFile = tmp.fileSync({ prefix: "jup-install-", postfix: ".sh" });
    const scriptPath = tmpFile.name;
    so we still have a scriptPath string as before, but now it is a securely created, unique file.

No other logic in updateBinary needs to change; writeFile, execSync, and the rm in the finally block can all continue to use scriptPath.


Suggested changeset 2
src/commands/UpdateCommand.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/commands/UpdateCommand.ts b/src/commands/UpdateCommand.ts
--- a/src/commands/UpdateCommand.ts
+++ b/src/commands/UpdateCommand.ts
@@ -5,6 +5,7 @@
 
 import chalk from "chalk";
 import ky from "ky";
+import tmp from "tmp";
 import type { Command } from "commander";
 
 import { version as currentVersion } from "../../package.json";
@@ -153,7 +154,8 @@
   private static async updateBinary(): Promise<void> {
     const scriptUrl =
       "https://github.com/jup-ag/cli/releases/latest/download/install.sh";
-    const scriptPath = join(tmpdir(), "jup-install.sh");
+    const tmpFile = tmp.fileSync({ prefix: "jup-install-", postfix: ".sh" });
+    const scriptPath = tmpFile.name;
 
     try {
       const script = await ky.get(scriptUrl).text();
EOF
@@ -5,6 +5,7 @@

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

import { version as currentVersion } from "../../package.json";
@@ -153,7 +154,8 @@
private static async updateBinary(): Promise<void> {
const scriptUrl =
"https://github.com/jup-ag/cli/releases/latest/download/install.sh";
const scriptPath = join(tmpdir(), "jup-install.sh");
const tmpFile = tmp.fileSync({ prefix: "jup-install-", postfix: ".sh" });
const scriptPath = tmpFile.name;

try {
const script = await ky.get(scriptUrl).text();
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -39,7 +39,8 @@
     "cli-table3": "^0.6.5",
     "commander": "^14.0.3",
     "ky": "^1.14.3",
-    "micro-key-producer": "^0.8.5"
+    "micro-key-producer": "^0.8.5",
+    "tmp": "^0.2.5"
   },
   "devDependencies": {
     "@types/bun": "latest",
EOF
@@ -39,7 +39,8 @@
"cli-table3": "^0.6.5",
"commander": "^14.0.3",
"ky": "^1.14.3",
"micro-key-producer": "^0.8.5"
"micro-key-producer": "^0.8.5",
"tmp": "^0.2.5"
},
"devDependencies": {
"@types/bun": "latest",
This fix introduces these dependencies
Package Version Security advisories
tmp (npm) 0.2.5 None
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in a follow-up. The updater now creates a unique temp directory with mkdtemp(...), writes install.sh inside that directory with mode 0o700, and removes the directory in finally, so the predictable temp-file path is gone.

Avoid predictable temp file path by creating a unique directory with
mkdtemp, preventing symlink attacks flagged by GitHub code scanning.
The script is already in the repo — import it as text so Bun bundles
it into the compiled binary. Removes the runtime network fetch.
Revert the text import approach — fetching from the release URL is
cleaner than importing a shell script as a TS text module.
Remove install method detection, package manager commands, and binary
update logic. The update command now fetches and runs install.sh which
handles volta/npm/binary fallback internally.
macOS 15+ ships a native /sbin/sha256sum, so the shasum -a 256
fallback for older versions is unnecessary.
@AaronCQL AaronCQL merged commit da6e74d into main Mar 24, 2026
4 checks passed
@AaronCQL AaronCQL deleted the pavs/update-command branch March 24, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants