|
#!/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] <command> [args...] |
|
|
|
Connection Options: |
|
--url <url> Upstash REST URL (or UPSTASH_REDIS_REST_URL env var) |
|
--token <token> Upstash REST token (or UPSTASH_REDIS_REST_TOKEN env var) |
|
-h, --help Show this help |
|
|
|
SET Options: |
|
--ex <seconds> Set expiration in seconds |
|
--px <ms> Set expiration in milliseconds |
|
--exat <ts> Set expiration as Unix timestamp (seconds) |
|
--pxat <ts> 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 <off,count> Limit results (e.g., --limit 0,10) |
|
|
|
General: |
|
--json Parse next argument as JSON (for complex objects) |
|
|
|
Commands: |
|
|
|
STRING: |
|
GET <key> |
|
SET <key> <value> [--ex N] [--px N] [--nx] [--xx] |
|
MGET <key1> [key2...] |
|
MSET <key1> <val1> [key2 val2...] |
|
INCR <key> |
|
INCRBY <key> <increment> |
|
DECR <key> |
|
DECRBY <key> <decrement> |
|
APPEND <key> <value> |
|
STRLEN <key> |
|
|
|
HASH: |
|
HGET <key> <field> |
|
HSET <key> <field1> <val1> [field2 val2...] |
|
HMGET <key> <field1> [field2...] |
|
HGETALL <key> |
|
HDEL <key> <field1> [field2...] |
|
HEXISTS <key> <field> |
|
HKEYS <key> |
|
HVALS <key> |
|
HLEN <key> |
|
HINCRBY <key> <field> <increment> |
|
HSETNX <key> <field> <value> |
|
|
|
LIST: |
|
LPUSH <key> <val1> [val2...] |
|
RPUSH <key> <val1> [val2...] |
|
LPOP <key> [count] |
|
RPOP <key> [count] |
|
LRANGE <key> <start> <stop> |
|
LLEN <key> |
|
LINDEX <key> <index> |
|
LSET <key> <index> <value> |
|
LREM <key> <count> <value> |
|
LTRIM <key> <start> <stop> |
|
LINSERT <key> <BEFORE|AFTER> <pivot> <value> |
|
|
|
SET: |
|
SADD <key> <member1> [member2...] |
|
SREM <key> <member1> [member2...] |
|
SMEMBERS <key> |
|
SISMEMBER <key> <member> |
|
SCARD <key> |
|
SPOP <key> [count] |
|
SRANDMEMBER <key> [count] |
|
SINTER <key1> [key2...] |
|
SUNION <key1> [key2...] |
|
SDIFF <key1> [key2...] |
|
|
|
SORTED SET: |
|
ZADD <key> <score1> <member1> [score2 member2...] [--nx] [--xx] [--gt] [--lt] [--ch] |
|
ZREM <key> <member1> [member2...] |
|
ZSCORE <key> <member> |
|
ZRANK <key> <member> |
|
ZREVRANK <key> <member> |
|
ZRANGE <key> <start> <stop> [--withscores] [--byscore] [--bylex] [--rev] |
|
ZRANGEBYSCORE <key> <min> <max> [--withscores] [--limit off,count] |
|
ZCARD <key> |
|
ZCOUNT <key> <min> <max> |
|
ZINCRBY <key> <increment> <member> |
|
ZPOPMIN <key> [count] |
|
ZPOPMAX <key> [count] |
|
|
|
KEY: |
|
DEL <key1> [key2...] |
|
EXISTS <key1> [key2...] |
|
EXPIRE <key> <seconds> |
|
EXPIREAT <key> <timestamp> |
|
TTL <key> |
|
PTTL <key> |
|
PERSIST <key> |
|
KEYS <pattern> |
|
SCAN <cursor> [MATCH pattern] [COUNT count] |
|
TYPE <key> |
|
RENAME <key> <newkey> |
|
RENAMENX <key> <newkey> |
|
|
|
SERVER: |
|
DBSIZE |
|
FLUSHDB |
|
PING |
|
ECHO <message> |
|
|
|
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<string, any> { |
|
const obj: Record<string, any> = {}; |
|
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<string, any> | undefined { |
|
const opts: Record<string, any> = {}; |
|
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<string, any> | undefined { |
|
const opts: Record<string, any> = {}; |
|
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<string, any> | undefined { |
|
const opts: Record<string, any> = {}; |
|
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<any> { |
|
// 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); |
|
} |