#!/usr/bin/env bun import { parseArgs } from "node:util"; import { Redis } from "@upstash/redis"; const { values, positionals } = parseArgs({ args: process.argv.slice(2), options: { url: { type: "string" }, token: { type: "string" }, help: { type: "boolean", short: "h" }, // SET options ex: { type: "string" }, px: { type: "string" }, exat: { type: "string" }, pxat: { type: "string" }, nx: { type: "boolean" }, xx: { type: "boolean" }, keepttl: { type: "boolean" }, get: { type: "boolean" }, // ZRANGE/ZADD options withscores: { type: "boolean" }, byscore: { type: "boolean" }, bylex: { type: "boolean" }, rev: { type: "boolean" }, limit: { type: "string" }, // ZADD-specific options gt: { type: "boolean" }, lt: { type: "boolean" }, ch: { type: "boolean" }, // JSON input for complex objects json: { type: "boolean" }, }, allowPositionals: true, }); function printHelp() { console.log(`Usage: bun run upstash-client.ts [options] [args...] Connection Options: --url Upstash REST URL (or UPSTASH_REDIS_REST_URL env var) --token Upstash REST token (or UPSTASH_REDIS_REST_TOKEN env var) -h, --help Show this help SET Options: --ex Set expiration in seconds --px Set expiration in milliseconds --exat Set expiration as Unix timestamp (seconds) --pxat Set expiration as Unix timestamp (milliseconds) --nx Only set if key does not exist --xx Only set if key already exists --keepttl Retain existing TTL --get Return old value ZADD Options: --nx Only add new elements --xx Only update existing elements --gt Only update when new score > current score --lt Only update when new score < current score --ch Return number of changed elements ZRANGE Options: --withscores Include scores in output --byscore Interpret range as score range --bylex Interpret range as lexicographical range --rev Reverse order --limit Limit results (e.g., --limit 0,10) General: --json Parse next argument as JSON (for complex objects) Commands: STRING: GET SET [--ex N] [--px N] [--nx] [--xx] MGET [key2...] MSET [key2 val2...] INCR INCRBY DECR DECRBY APPEND STRLEN HASH: HGET HSET [field2 val2...] HMGET [field2...] HGETALL HDEL [field2...] HEXISTS HKEYS HVALS HLEN HINCRBY HSETNX LIST: LPUSH [val2...] RPUSH [val2...] LPOP [count] RPOP [count] LRANGE LLEN LINDEX LSET LREM LTRIM LINSERT SET: SADD [member2...] SREM [member2...] SMEMBERS SISMEMBER SCARD SPOP [count] SRANDMEMBER [count] SINTER [key2...] SUNION [key2...] SDIFF [key2...] SORTED SET: ZADD [score2 member2...] [--nx] [--xx] [--gt] [--lt] [--ch] ZREM [member2...] ZSCORE ZRANK ZREVRANK ZRANGE [--withscores] [--byscore] [--bylex] [--rev] ZRANGEBYSCORE [--withscores] [--limit off,count] ZCARD ZCOUNT ZINCRBY ZPOPMIN [count] ZPOPMAX [count] KEY: DEL [key2...] EXISTS [key2...] EXPIRE EXPIREAT TTL PTTL PERSIST KEYS SCAN [MATCH pattern] [COUNT count] TYPE RENAME RENAMENX SERVER: DBSIZE FLUSHDB PING ECHO Examples: # Basic string operations bun run upstash-client.ts SET mykey "hello world" bun run upstash-client.ts GET mykey bun run upstash-client.ts SET session:123 "data" --ex 3600 # Hash operations (field/value pairs) bun run upstash-client.ts HSET user:1 name "John" email "john@example.com" age 30 bun run upstash-client.ts HGET user:1 name bun run upstash-client.ts HGETALL user:1 # List operations bun run upstash-client.ts LPUSH tasks "task1" "task2" "task3" bun run upstash-client.ts LRANGE tasks 0 -1 # Set operations bun run upstash-client.ts SADD tags "javascript" "redis" "nodejs" bun run upstash-client.ts SMEMBERS tags # Sorted set operations (score member pairs) bun run upstash-client.ts ZADD leaderboard 1000 "player1" 1500 "player2" bun run upstash-client.ts ZRANGE leaderboard 0 -1 --withscores --rev # Multiple keys bun run upstash-client.ts MSET key1 "val1" key2 "val2" key3 "val3" bun run upstash-client.ts MGET key1 key2 key3 # With explicit credentials bun run upstash-client.ts --url https://... --token AX... GET mykey`); } if (values.help || positionals.length === 0) { printHelp(); process.exit(values.help ? 0 : 1); } const url = values.url || process.env.UPSTASH_REDIS_REST_URL; const token = values.token || process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) { console.error("Error: Missing credentials"); console.error("Provide --url and --token, or set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN"); process.exit(1); } const redis = new Redis({ url, token }); const [rawCmd, ...args] = positionals; const cmd = rawCmd.toUpperCase(); // Helper to parse a value (try JSON, fall back to string/number) function parseValue(val: string): any { if (values.json) { try { return JSON.parse(val); } catch { return val; } } // Try to parse as number const num = Number(val); if (!isNaN(num) && val.trim() !== "") { return num; } // Try to parse as JSON object/array if ((val.startsWith("{") && val.endsWith("}")) || (val.startsWith("[") && val.endsWith("]"))) { try { return JSON.parse(val); } catch { return val; } } return val; } // Helper to convert pairs to object: [k1, v1, k2, v2] -> {k1: v1, k2: v2} function pairsToObject(pairs: string[]): Record { const obj: Record = {}; for (let i = 0; i < pairs.length; i += 2) { if (i + 1 < pairs.length) { obj[pairs[i]] = parseValue(pairs[i + 1]); } } return obj; } // Helper to convert score/member pairs: [s1, m1, s2, m2] -> [{score: s1, member: m1}, ...] function scoreMemberPairs(pairs: string[]): Array<{ score: number; member: string }> { const result: Array<{ score: number; member: string }> = []; for (let i = 0; i < pairs.length; i += 2) { if (i + 1 < pairs.length) { result.push({ score: Number(pairs[i]), member: pairs[i + 1] }); } } return result; } // Build SET options function buildSetOptions(): Record | undefined { const opts: Record = {}; if (values.ex) opts.ex = Number(values.ex); if (values.px) opts.px = Number(values.px); if (values.exat) opts.exat = Number(values.exat); if (values.pxat) opts.pxat = Number(values.pxat); if (values.nx) opts.nx = true; if (values.xx) opts.xx = true; if (values.keepttl) opts.keepttl = true; if (values.get) opts.get = true; return Object.keys(opts).length > 0 ? opts : undefined; } // Build ZADD options function buildZaddOptions(): Record | undefined { const opts: Record = {}; if (values.nx) opts.nx = true; if (values.xx) opts.xx = true; if (values.gt) opts.gt = true; if (values.lt) opts.lt = true; if (values.ch) opts.ch = true; return Object.keys(opts).length > 0 ? opts : undefined; } // Build ZRANGE options function buildZrangeOptions(): Record | undefined { const opts: Record = {}; if (values.withscores) opts.withScores = true; if (values.byscore) opts.byScore = true; if (values.bylex) opts.byLex = true; if (values.rev) opts.rev = true; if (values.limit) { const [offset, count] = values.limit.split(",").map(Number); opts.offset = offset; opts.count = count; } return Object.keys(opts).length > 0 ? opts : undefined; } // Parse SCAN arguments: SCAN cursor [MATCH pattern] [COUNT count] function parseScanArgs(args: string[]): { cursor: number; options?: { match?: string; count?: number } } { const cursor = Number(args[0]) || 0; const options: { match?: string; count?: number } = {}; for (let i = 1; i < args.length; i++) { const arg = args[i].toUpperCase(); if (arg === "MATCH" && i + 1 < args.length) { options.match = args[++i]; } else if (arg === "COUNT" && i + 1 < args.length) { options.count = Number(args[++i]); } } return { cursor, options: Object.keys(options).length > 0 ? options : undefined }; } async function execute(): Promise { // STRING COMMANDS switch (cmd) { case "GET": return redis.get(args[0]); case "SET": { const opts = buildSetOptions(); return opts ? redis.set(args[0], parseValue(args[1]), opts) : redis.set(args[0], parseValue(args[1])); } case "SETNX": return redis.setnx(args[0], parseValue(args[1])); case "SETEX": return redis.setex(args[0], Number(args[1]), parseValue(args[2])); case "PSETEX": return redis.psetex(args[0], Number(args[1]), parseValue(args[2])); case "MGET": return redis.mget(...args); case "MSET": return redis.mset(pairsToObject(args)); case "MSETNX": return redis.msetnx(pairsToObject(args)); case "INCR": return redis.incr(args[0]); case "INCRBY": return redis.incrby(args[0], Number(args[1])); case "INCRBYFLOAT": return redis.incrbyfloat(args[0], Number(args[1])); case "DECR": return redis.decr(args[0]); case "DECRBY": return redis.decrby(args[0], Number(args[1])); case "APPEND": return redis.append(args[0], args[1]); case "STRLEN": return redis.strlen(args[0]); case "GETRANGE": return redis.getrange(args[0], Number(args[1]), Number(args[2])); case "SETRANGE": return redis.setrange(args[0], Number(args[1]), args[2]); // HASH COMMANDS case "HGET": return redis.hget(args[0], args[1]); case "HSET": return redis.hset(args[0], pairsToObject(args.slice(1))); case "HSETNX": return redis.hsetnx(args[0], args[1], parseValue(args[2])); case "HMSET": return redis.hset(args[0], pairsToObject(args.slice(1))); case "HMGET": return redis.hmget(args[0], ...args.slice(1)); case "HGETALL": return redis.hgetall(args[0]); case "HDEL": return redis.hdel(args[0], ...args.slice(1)); case "HEXISTS": return redis.hexists(args[0], args[1]); case "HKEYS": return redis.hkeys(args[0]); case "HVALS": return redis.hvals(args[0]); case "HLEN": return redis.hlen(args[0]); case "HINCRBY": return redis.hincrby(args[0], args[1], Number(args[2])); case "HINCRBYFLOAT": return redis.hincrbyfloat(args[0], args[1], Number(args[2])); case "HSCAN": { const cursor = Number(args[1]) || 0; const scanOpts: { match?: string; count?: number } = {}; for (let i = 2; i < args.length; i++) { if (args[i].toUpperCase() === "MATCH" && i + 1 < args.length) scanOpts.match = args[++i]; if (args[i].toUpperCase() === "COUNT" && i + 1 < args.length) scanOpts.count = Number(args[++i]); } return redis.hscan(args[0], cursor, Object.keys(scanOpts).length > 0 ? scanOpts : undefined); } // LIST COMMANDS case "LPUSH": return redis.lpush(args[0], ...args.slice(1).map(parseValue)); case "RPUSH": return redis.rpush(args[0], ...args.slice(1).map(parseValue)); case "LPUSHX": return redis.lpushx(args[0], ...args.slice(1).map(parseValue)); case "RPUSHX": return redis.rpushx(args[0], ...args.slice(1).map(parseValue)); case "LPOP": return args.length > 1 ? redis.lpop(args[0], Number(args[1])) : redis.lpop(args[0]); case "RPOP": return args.length > 1 ? redis.rpop(args[0], Number(args[1])) : redis.rpop(args[0]); case "LRANGE": return redis.lrange(args[0], Number(args[1]), Number(args[2])); case "LLEN": return redis.llen(args[0]); case "LINDEX": return redis.lindex(args[0], Number(args[1])); case "LSET": return redis.lset(args[0], Number(args[1]), parseValue(args[2])); case "LREM": return redis.lrem(args[0], Number(args[1]), parseValue(args[2])); case "LTRIM": return redis.ltrim(args[0], Number(args[1]), Number(args[2])); case "LINSERT": return redis.linsert( args[0], args[1].toLowerCase() as "before" | "after", parseValue(args[2]), parseValue(args[3]) ); case "LPOS": return redis.lpos(args[0], parseValue(args[1])); case "LMOVE": return redis.lmove( args[0], args[1], args[2].toUpperCase() as "LEFT" | "RIGHT", args[3].toUpperCase() as "LEFT" | "RIGHT" ); // SET COMMANDS case "SADD": return redis.sadd(args[0], ...args.slice(1).map(parseValue)); case "SREM": return redis.srem(args[0], ...args.slice(1).map(parseValue)); case "SMEMBERS": return redis.smembers(args[0]); case "SISMEMBER": return redis.sismember(args[0], parseValue(args[1])); case "SMISMEMBER": return redis.smismember(args[0], args.slice(1).map(parseValue)); case "SCARD": return redis.scard(args[0]); case "SPOP": return args.length > 1 ? redis.spop(args[0], Number(args[1])) : redis.spop(args[0]); case "SRANDMEMBER": return args.length > 1 ? redis.srandmember(args[0], Number(args[1])) : redis.srandmember(args[0]); case "SINTER": return redis.sinter(...args); case "SINTERSTORE": return redis.sinterstore(args[0], ...args.slice(1)); case "SUNION": return redis.sunion(...args); case "SUNIONSTORE": return redis.sunionstore(args[0], ...args.slice(1)); case "SDIFF": return redis.sdiff(...args); case "SDIFFSTORE": return redis.sdiffstore(args[0], ...args.slice(1)); case "SMOVE": return redis.smove(args[0], args[1], parseValue(args[2])); case "SSCAN": { const cursor = Number(args[1]) || 0; const scanOpts: { match?: string; count?: number } = {}; for (let i = 2; i < args.length; i++) { if (args[i].toUpperCase() === "MATCH" && i + 1 < args.length) scanOpts.match = args[++i]; if (args[i].toUpperCase() === "COUNT" && i + 1 < args.length) scanOpts.count = Number(args[++i]); } return redis.sscan(args[0], cursor, Object.keys(scanOpts).length > 0 ? scanOpts : undefined); } // SORTED SET COMMANDS case "ZADD": { const zaddOpts = buildZaddOptions(); const members = scoreMemberPairs(args.slice(1)); return zaddOpts ? redis.zadd(args[0], zaddOpts, ...members) : redis.zadd(args[0], ...members); } case "ZREM": return redis.zrem(args[0], ...args.slice(1)); case "ZSCORE": return redis.zscore(args[0], args[1]); case "ZMSCORE": return redis.zmscore(args[0], ...args.slice(1)); case "ZRANK": return redis.zrank(args[0], args[1]); case "ZREVRANK": return redis.zrevrank(args[0], args[1]); case "ZRANGE": { const opts = buildZrangeOptions(); return opts ? redis.zrange(args[0], args[1], args[2], opts) : redis.zrange(args[0], args[1], args[2]); } case "ZRANGEBYSCORE": { const opts: { withScores?: boolean; offset?: number; count?: number } = {}; if (values.withscores) opts.withScores = true; if (values.limit) { const [offset, count] = values.limit.split(",").map(Number); opts.offset = offset; opts.count = count; } return Object.keys(opts).length > 0 ? redis.zrange(args[0], args[1], args[2], { byScore: true, ...opts }) : redis.zrange(args[0], args[1], args[2], { byScore: true }); } case "ZREVRANGE": { const opts = buildZrangeOptions(); return redis.zrange(args[0], args[1], args[2], { rev: true, ...opts }); } case "ZREVRANGEBYSCORE": { const opts: { withScores?: boolean; offset?: number; count?: number } = {}; if (values.withscores) opts.withScores = true; if (values.limit) { const [offset, count] = values.limit.split(",").map(Number); opts.offset = offset; opts.count = count; } return redis.zrange(args[0], args[2], args[1], { byScore: true, rev: true, ...opts }); } case "ZCARD": return redis.zcard(args[0]); case "ZCOUNT": return redis.zcount(args[0], args[1], args[2]); case "ZINCRBY": return redis.zincrby(args[0], Number(args[1]), args[2]); case "ZPOPMIN": return args.length > 1 ? redis.zpopmin(args[0], Number(args[1])) : redis.zpopmin(args[0]); case "ZPOPMAX": return args.length > 1 ? redis.zpopmax(args[0], Number(args[1])) : redis.zpopmax(args[0]); case "ZREMRANGEBYRANK": return redis.zremrangebyrank(args[0], Number(args[1]), Number(args[2])); case "ZREMRANGEBYSCORE": return redis.zremrangebyscore(args[0], args[1], args[2]); case "ZINTERSTORE": return redis.zinterstore(args[0], Number(args[1]), ...args.slice(2)); case "ZUNIONSTORE": return redis.zunionstore(args[0], Number(args[1]), ...args.slice(2)); case "ZSCAN": { const cursor = Number(args[1]) || 0; const scanOpts: { match?: string; count?: number } = {}; for (let i = 2; i < args.length; i++) { if (args[i].toUpperCase() === "MATCH" && i + 1 < args.length) scanOpts.match = args[++i]; if (args[i].toUpperCase() === "COUNT" && i + 1 < args.length) scanOpts.count = Number(args[++i]); } return redis.zscan(args[0], cursor, Object.keys(scanOpts).length > 0 ? scanOpts : undefined); } // KEY COMMANDS case "DEL": return redis.del(...args); case "UNLINK": return redis.unlink(...args); case "EXISTS": return redis.exists(...args); case "EXPIRE": return redis.expire(args[0], Number(args[1])); case "EXPIREAT": return redis.expireat(args[0], Number(args[1])); case "PEXPIRE": return redis.pexpire(args[0], Number(args[1])); case "PEXPIREAT": return redis.pexpireat(args[0], Number(args[1])); case "TTL": return redis.ttl(args[0]); case "PTTL": return redis.pttl(args[0]); case "PERSIST": return redis.persist(args[0]); case "KEYS": return redis.keys(args[0]); case "SCAN": { const { cursor, options } = parseScanArgs(args); return options ? redis.scan(cursor, options) : redis.scan(cursor); } case "TYPE": return redis.type(args[0]); case "RENAME": return redis.rename(args[0], args[1]); case "RENAMENX": return redis.renamenx(args[0], args[1]); case "COPY": return redis.copy(args[0], args[1]); case "DUMP": return redis.dump(args[0]); case "OBJECT": { const subcmd = args[0].toUpperCase(); if (subcmd === "ENCODING") return redis.objectEncoding(args[1]); if (subcmd === "FREQ") return redis.objectFreq(args[1]); if (subcmd === "IDLETIME") return redis.objectIdletime(args[1]); if (subcmd === "REFCOUNT") return redis.objectRefcount(args[1]); throw new Error(`Unknown OBJECT subcommand: ${subcmd}`); } case "RANDOMKEY": return redis.randomkey(); case "TOUCH": return redis.touch(...args); // SERVER COMMANDS case "DBSIZE": return redis.dbsize(); case "FLUSHDB": return redis.flushdb(); case "FLUSHALL": return redis.flushall(); case "PING": return args.length > 0 ? redis.ping(args[0]) : redis.ping(); case "ECHO": return redis.echo(args[0]); case "TIME": return redis.time(); case "INFO": return redis.info(args[0]); // JSON COMMANDS (if using Upstash JSON) case "JSON.SET": return (redis as any).json.set(args[0], args[1], parseValue(args[2])); case "JSON.GET": return (redis as any).json.get(args[0], args.length > 1 ? args[1] : "$"); case "JSON.DEL": return (redis as any).json.del(args[0], args.length > 1 ? args[1] : "$"); default: throw new Error(`Unknown or unsupported command: ${cmd}`); } } try { const result = await execute(); if (result === null || result === undefined) { console.log("(nil)"); } else if (typeof result === "object") { console.log(JSON.stringify(result, null, 2)); } else { console.log(result); } } catch (err) { console.error(`Error: ${err instanceof Error ? err.message : err}`); process.exit(1); }