464 lines
16 KiB
TypeScript
464 lines
16 KiB
TypeScript
/**
|
|
* Static Redis command metadata, distilled from the official Redis command table
|
|
* (redis-doc `commands.json` / the `COMMAND` output). Kept offline and lightweight:
|
|
* we only retain what the editor syntax diagnostics need — `arity` and `group` —
|
|
* plus a `safety` hint for danger/write-command highlighting.
|
|
*
|
|
* Arity semantics (matches the Redis `COMMAND` reply, command name INCLUDED in the count):
|
|
* arity > 0 → the command takes exactly `arity` tokens (e.g. GET key → arity 2).
|
|
* arity < 0 → the command takes AT LEAST `-arity` tokens (e.g. MSET k v [k v ...] → arity -3).
|
|
*
|
|
* `safety` is aligned with the token-level sets in `redisCommandSafety.ts` (used to gate
|
|
* execution). Here it is recorded per-command (including subcommands) so diagnostics can
|
|
* be more precise than the first-token classification.
|
|
*
|
|
* Subcommands are keyed as `"MAIN SUB"` in UPPER CASE (e.g. `"CONFIG GET"`, `"XGROUP CREATE"`).
|
|
*/
|
|
import type { RedisCommandSafety } from "@/lib/redisCommandSafety";
|
|
|
|
export interface RedisCommandSpec {
|
|
/** Token count including the command name. Positive = exact, negative = minimum (-N). */
|
|
arity: number;
|
|
/** Redis command group: string/list/hash/.../server. */
|
|
group: string;
|
|
/** Danger level for diagnostic highlighting. */
|
|
safety: RedisCommandSafety;
|
|
}
|
|
|
|
type Spec = [arity: number, group: string, safety?: RedisCommandSafety];
|
|
|
|
// Compact tuple form → expanded into the record below.
|
|
const RAW_COMMANDS: Record<string, Spec> = {
|
|
// ---- String ----
|
|
APPEND: [3, "string", "confirm"],
|
|
DECR: [2, "string", "confirm"],
|
|
DECRBY: [3, "string", "confirm"],
|
|
GET: [2, "string"],
|
|
GETDEL: [2, "string", "confirm"],
|
|
GETEX: [-2, "string"],
|
|
GETRANGE: [4, "string"],
|
|
GETSET: [3, "string", "confirm"],
|
|
INCR: [2, "string", "confirm"],
|
|
INCRBY: [3, "string", "confirm"],
|
|
INCRBYFLOAT: [3, "string", "confirm"],
|
|
LCS: [-3, "string"],
|
|
MGET: [-2, "string"],
|
|
MSET: [-3, "string", "confirm"],
|
|
MSETNX: [-3, "string", "confirm"],
|
|
PSETEX: [4, "string", "confirm"],
|
|
SET: [-3, "string", "confirm"],
|
|
SETEX: [4, "string", "confirm"],
|
|
SETNX: [3, "string", "confirm"],
|
|
SETRANGE: [4, "string", "confirm"],
|
|
STRLEN: [2, "string"],
|
|
|
|
// ---- Generic key ----
|
|
COPY: [-3, "generic", "confirm"],
|
|
DEL: [-2, "generic", "confirm"],
|
|
DUMP: [2, "generic"],
|
|
EXISTS: [-2, "generic"],
|
|
EXPIRE: [-3, "generic", "confirm"],
|
|
EXPIREAT: [-3, "generic", "confirm"],
|
|
KEYS: [2, "generic", "blocked"],
|
|
MIGRATE: [-6, "generic", "blocked"],
|
|
MOVE: [3, "generic", "confirm"],
|
|
OBJECT_ENCODING: [3, "generic"],
|
|
OBJECT_FREQ: [3, "generic"],
|
|
OBJECT_IDLETIME: [3, "generic"],
|
|
OBJECT_REFCOUNT: [3, "generic"],
|
|
OBJECT_HELP: [2, "generic"],
|
|
PERSIST: [2, "generic", "confirm"],
|
|
PEXPIRE: [-3, "generic", "confirm"],
|
|
PEXPIREAT: [-3, "generic", "confirm"],
|
|
PTTL: [2, "generic"],
|
|
RANDOMKEY: [1, "generic"],
|
|
RENAME: [3, "generic", "confirm"],
|
|
RENAMENX: [3, "generic", "confirm"],
|
|
RESTORE: [-4, "generic", "confirm"],
|
|
SCAN: [-2, "generic"],
|
|
SORT: [-2, "generic", "confirm"],
|
|
SORT_RO: [-2, "generic"],
|
|
TOUCH: [-2, "generic"],
|
|
TTL: [2, "generic"],
|
|
TYPE: [2, "generic"],
|
|
UNLINK: [-2, "generic", "confirm"],
|
|
WAIT: [3, "generic"],
|
|
WAITAOF: [4, "generic"],
|
|
|
|
// ---- List ----
|
|
BLMOVE: [6, "list"],
|
|
BLMPOP: [-4, "list"],
|
|
BLPOP: [-3, "list"],
|
|
BRPOP: [-3, "list"],
|
|
BRPOPLPUSH: [4, "list"],
|
|
LINDEX: [3, "list"],
|
|
LINSERT: [5, "list", "confirm"],
|
|
LLEN: [2, "list"],
|
|
LMOVE: [5, "list", "confirm"],
|
|
LMPOP: [-4, "list", "confirm"],
|
|
LPOP: [-2, "list", "confirm"],
|
|
LPOS: [-2, "list"],
|
|
LPUSH: [-3, "list", "confirm"],
|
|
LPUSHX: [-3, "list", "confirm"],
|
|
LRANGE: [4, "list"],
|
|
LREM: [4, "list", "confirm"],
|
|
LSET: [4, "list", "confirm"],
|
|
LTRIM: [4, "list", "confirm"],
|
|
RPOP: [-2, "list", "confirm"],
|
|
RPOPLPUSH: [4, "list", "confirm"],
|
|
RPUSH: [-3, "list", "confirm"],
|
|
RPUSHX: [-3, "list", "confirm"],
|
|
|
|
// ---- Hash ----
|
|
HDEL: [-3, "hash", "confirm"],
|
|
HEXISTS: [3, "hash"],
|
|
HGET: [3, "hash"],
|
|
HGETALL: [2, "hash"],
|
|
HINCRBY: [4, "hash", "confirm"],
|
|
HINCRBYFLOAT: [4, "hash", "confirm"],
|
|
HKEYS: [2, "hash"],
|
|
HLEN: [2, "hash"],
|
|
HMGET: [-3, "hash"],
|
|
HMSET: [-4, "hash", "confirm"],
|
|
HRANDFIELD: [-2, "hash"],
|
|
HSCAN: [-3, "hash"],
|
|
HSET: [-4, "hash", "confirm"],
|
|
HSETNX: [4, "hash", "confirm"],
|
|
HSTRLEN: [3, "hash"],
|
|
HVALS: [2, "hash"],
|
|
|
|
// ---- Set ----
|
|
SADD: [-3, "set", "confirm"],
|
|
SCARD: [2, "set"],
|
|
SDIFF: [-2, "set"],
|
|
SDIFFSTORE: [-3, "set", "confirm"],
|
|
SINTER: [-2, "set"],
|
|
SINTERCARD: [-3, "set"],
|
|
SINTERSTORE: [-3, "set", "confirm"],
|
|
SISMEMBER: [3, "set"],
|
|
SMEMBERS: [2, "set"],
|
|
SMISMEMBER: [-3, "set"],
|
|
SMOVE: [4, "set", "confirm"],
|
|
SPOP: [-2, "set", "confirm"],
|
|
SRANDMEMBER: [-2, "set"],
|
|
SREM: [-3, "set", "confirm"],
|
|
SSCAN: [-3, "set"],
|
|
SUNION: [-2, "set"],
|
|
SUNIONSTORE: [-3, "set", "confirm"],
|
|
|
|
// ---- Sorted set ----
|
|
BZMPOP: [-3, "zset"],
|
|
BZPOPMAX: [-3, "zset"],
|
|
BZPOPMIN: [-3, "zset"],
|
|
ZADD: [-4, "zset", "confirm"],
|
|
ZCARD: [2, "zset"],
|
|
ZCOUNT: [4, "zset"],
|
|
ZDIFF: [-3, "zset"],
|
|
ZDIFFSTORE: [-4, "zset", "confirm"],
|
|
ZINCRBY: [4, "zset", "confirm"],
|
|
ZINTER: [-3, "zset"],
|
|
ZINTERCARD: [-3, "zset"],
|
|
ZINTERSTORE: [-4, "zset", "confirm"],
|
|
ZLEXCOUNT: [4, "zset"],
|
|
ZMPOP: [-3, "zset", "confirm"],
|
|
ZMSCORE: [-3, "zset"],
|
|
ZPOPMAX: [-2, "zset", "confirm"],
|
|
ZPOPMIN: [-2, "zset", "confirm"],
|
|
ZRANGE: [-4, "zset"],
|
|
ZRANGEBYLEX: [-4, "zset"],
|
|
ZRANGEBYSCORE: [-4, "zset"],
|
|
ZRANGESTORE: [-5, "zset", "confirm"],
|
|
ZRANK: [-3, "zset"],
|
|
ZREM: [-3, "zset", "confirm"],
|
|
ZREMRANGEBYLEX: [4, "zset", "confirm"],
|
|
ZREMRANGEBYRANK: [4, "zset", "confirm"],
|
|
ZREMRANGEBYSCORE: [4, "zset", "confirm"],
|
|
ZREVRANGE: [-4, "zset"],
|
|
ZREVRANGEBYLEX: [-4, "zset"],
|
|
ZREVRANGEBYSCORE: [-4, "zset"],
|
|
ZREVRANK: [-3, "zset"],
|
|
ZSCAN: [-3, "zset"],
|
|
ZSCORE: [3, "zset"],
|
|
ZUNION: [-3, "zset"],
|
|
ZUNIONSTORE: [-4, "zset", "confirm"],
|
|
|
|
// ---- Bitmap ----
|
|
BITCOUNT: [-2, "bitmap"],
|
|
BITFIELD: [-2, "bitmap", "confirm"],
|
|
BITFIELD_RO: [-2, "bitmap"],
|
|
BITOP: [-4, "bitmap", "confirm"],
|
|
BITPOS: [-3, "bitmap"],
|
|
GETBIT: [3, "bitmap"],
|
|
SETBIT: [4, "bitmap", "confirm"],
|
|
|
|
// ---- Hyperloglog ----
|
|
PFADD: [-2, "hyperloglog", "confirm"],
|
|
PFCOUNT: [-2, "hyperloglog"],
|
|
PFMERGE: [-2, "hyperloglog", "confirm"],
|
|
|
|
// ---- Geo ----
|
|
GEOADD: [-5, "geo", "confirm"],
|
|
GEODIST: [-4, "geo"],
|
|
GEOHASH: [-3, "geo"],
|
|
GEOPOS: [-3, "geo"],
|
|
GEORADIUS: [-6, "geo", "confirm"],
|
|
GEORADIUS_RO: [-6, "geo"],
|
|
GEORADIUSBYMEMBER: [-5, "geo", "confirm"],
|
|
GEORADIUSBYMEMBER_RO: [-5, "geo"],
|
|
GEOSEARCH: [-7, "geo"],
|
|
GEOSEARCHSTORE: [-8, "geo", "confirm"],
|
|
|
|
// ---- Stream ----
|
|
XACK: [-4, "stream", "confirm"],
|
|
XADD: [-5, "stream", "confirm"],
|
|
XAUTOCLAIM: [-7, "stream", "confirm"],
|
|
XCLAIM: [-6, "stream", "confirm"],
|
|
XDEL: [-3, "stream", "confirm"],
|
|
"XGROUP CREATE": [-6, "stream", "confirm"],
|
|
"XGROUP CREATECONSUMER": [5, "stream", "confirm"],
|
|
"XGROUP DELCONSUMER": [4, "stream", "confirm"],
|
|
"XGROUP DESTROY": [4, "stream", "confirm"],
|
|
"XGROUP HELP": [2, "stream"],
|
|
"XGROUP SETID": [-5, "stream", "confirm"],
|
|
"XINFO CONSUMERS": [4, "stream"],
|
|
"XINFO GROUPS": [3, "stream"],
|
|
"XINFO HELP": [2, "stream"],
|
|
"XINFO STREAM": [-3, "stream"],
|
|
XLEN: [2, "stream"],
|
|
XPENDING: [-3, "stream"],
|
|
XRANGE: [-4, "stream"],
|
|
XREAD: [-4, "stream"],
|
|
XREADGROUP: [-7, "stream"],
|
|
XREVRANGE: [-4, "stream"],
|
|
XSETID: [-3, "stream", "confirm"],
|
|
XTRIM: [-4, "stream", "confirm"],
|
|
|
|
// ---- Pub/Sub ----
|
|
PSUBSCRIBE: [-2, "pubsub"],
|
|
PUBLISH: [3, "pubsub"],
|
|
"PUBSUB CHANNELS": [-2, "pubsub"],
|
|
"PUBSUB NUMPAT": [1, "pubsub"],
|
|
"PUBSUB NUMSUB": [-2, "pubsub"],
|
|
"PUBSUB SHARDCHANNELS": [-2, "pubsub"],
|
|
"PUBSUB SHARDNUMSUB": [-2, "pubsub"],
|
|
PUNSUBSCRIBE: [-1, "pubsub"],
|
|
SPUBLISH: [3, "pubsub"],
|
|
SSUBSCRIBE: [2, "pubsub"],
|
|
SUBSCRIBE: [-2, "pubsub"],
|
|
SUNSUBSCRIBE: [-1, "pubsub"],
|
|
UNSUBSCRIBE: [-1, "pubsub"],
|
|
|
|
// ---- Transactions ----
|
|
DISCARD: [1, "transactions"],
|
|
EXEC: [1, "transactions"],
|
|
MULTI: [1, "transactions"],
|
|
UNWATCH: [1, "transactions"],
|
|
WATCH: [-2, "transactions"],
|
|
|
|
// ---- Scripting ----
|
|
EVAL: [-3, "scripting", "blocked"],
|
|
EVALSHA: [-3, "scripting", "blocked"],
|
|
EVAL_RO: [-3, "scripting", "blocked"],
|
|
EVALSHA_RO: [-3, "scripting", "blocked"],
|
|
FCALL: [-3, "scripting"],
|
|
FCALL_RO: [-3, "scripting"],
|
|
"FUNCTION DELETE": [3, "scripting", "confirm"],
|
|
"FUNCTION DUMP": [2, "scripting"],
|
|
"FUNCTION FLUSH": [-2, "scripting", "confirm"],
|
|
"FUNCTION LIST": [-2, "scripting"],
|
|
"FUNCTION LOAD": [-3, "scripting", "confirm"],
|
|
"FUNCTION RESTORE": [-4, "scripting", "confirm"],
|
|
"FUNCTION STATS": [1, "scripting"],
|
|
"SCRIPT EXISTS": [-2, "scripting", "blocked"],
|
|
"SCRIPT FLUSH": [-2, "scripting", "blocked"],
|
|
"SCRIPT KILL": [1, "scripting", "blocked"],
|
|
"SCRIPT LOAD": [2, "scripting", "blocked"],
|
|
|
|
// ---- Connection ----
|
|
AUTH: [-2, "connection"],
|
|
ECHO: [2, "connection"],
|
|
HELLO: [-1, "connection"],
|
|
PING: [-1, "connection"],
|
|
QUIT: [1, "connection"],
|
|
RESET: [1, "connection"],
|
|
SELECT: [2, "connection"],
|
|
"CLIENT CACHING": [3, "connection"],
|
|
"CLIENT GETNAME": [1, "connection"],
|
|
"CLIENT GETREDIR": [1, "connection"],
|
|
"CLIENT ID": [1, "connection"],
|
|
"CLIENT INFO": [1, "connection"],
|
|
"CLIENT KILL": [-3, "connection", "confirm"],
|
|
"CLIENT LIST": [-2, "connection"],
|
|
"CLIENT NO-EVICT": [2, "connection"],
|
|
"CLIENT NO-TOUCH": [2, "connection"],
|
|
"CLIENT PAUSE": [-2, "connection"],
|
|
"CLIENT REPLY": [2, "connection"],
|
|
"CLIENT SETINFO": [-3, "connection"],
|
|
"CLIENT SETNAME": [3, "connection"],
|
|
"CLIENT TRACKING": [-3, "connection"],
|
|
"CLIENT TRACKINGINFO": [1, "connection"],
|
|
"CLIENT UNPAUSE": [1, "connection"],
|
|
|
|
// ---- Server ----
|
|
BGREWRITEAOF: [1, "server"],
|
|
BGSAVE: [-1, "server", "blocked"],
|
|
"COMMAND COUNT": [1, "server"],
|
|
"COMMAND DOCS": [-2, "server"],
|
|
"COMMAND GETKEYS": [-3, "server"],
|
|
"COMMAND INFO": [-2, "server"],
|
|
"COMMAND LIST": [-2, "server"],
|
|
"CONFIG GET": [-2, "server", "blocked"],
|
|
"CONFIG RESETSTAT": [1, "server", "blocked"],
|
|
"CONFIG REWRITE": [1, "server", "blocked"],
|
|
"CONFIG SET": [-3, "server", "blocked"],
|
|
DBSIZE: [1, "server"],
|
|
FAILOVER: [-1, "server", "confirm"],
|
|
FLUSHALL: [-1, "server", "blocked"],
|
|
FLUSHDB: [-1, "server", "confirm"],
|
|
INFO: [-1, "server"],
|
|
LASTSAVE: [1, "server"],
|
|
"MEMORY USAGE": [-3, "server"],
|
|
"MEMORY STATS": [1, "server"],
|
|
"MEMORY PURGE": [1, "server"],
|
|
"MEMORY DOCS": [1, "server"],
|
|
"MODULE LIST": [1, "server", "blocked"],
|
|
"MODULE LOAD": [-3, "server", "blocked"],
|
|
"MODULE LOADEX": [-3, "server", "blocked"],
|
|
"MODULE UNLOAD": [3, "server", "blocked"],
|
|
MONITOR: [1, "server"],
|
|
REPLICAOF: [3, "server", "blocked"],
|
|
ROLE: [1, "server"],
|
|
SAVE: [1, "server", "blocked"],
|
|
SHUTDOWN: [-1, "server", "blocked"],
|
|
SLAVEOF: [3, "server", "blocked"],
|
|
"SLOWLOG GET": [-2, "server"],
|
|
"SLOWLOG LEN": [1, "server"],
|
|
"SLOWLOG RESET": [1, "server"],
|
|
"SLOWLOG HELP": [1, "server"],
|
|
SWAPDB: [3, "server", "confirm"],
|
|
SYNC: [1, "server"],
|
|
TIME: [1, "server"],
|
|
|
|
// ---- Cluster ----
|
|
"CLUSTER ADDSLOTS": [-3, "cluster", "confirm"],
|
|
"CLUSTER COUNT-FAILURE-REPORTS": [2, "cluster"],
|
|
"CLUSTER COUNTKEYSINSLOT": [3, "cluster"],
|
|
"CLUSTER DELSLOTS": [-3, "cluster", "confirm"],
|
|
"CLUSTER FAILOVER": [-2, "cluster", "confirm"],
|
|
"CLUSTER FLUSHSLOTS": [1, "cluster", "confirm"],
|
|
"CLUSTER FORGET": [2, "cluster", "confirm"],
|
|
"CLUSTER GETKEYSINSLOT": [4, "cluster"],
|
|
"CLUSTER INFO": [1, "cluster"],
|
|
"CLUSTER KEYSLOT": [2, "cluster"],
|
|
"CLUSTER LINKS": [1, "cluster"],
|
|
"CLUSTER MEET": [-4, "cluster", "confirm"],
|
|
"CLUSTER MYID": [1, "cluster"],
|
|
"CLUSTER NODES": [1, "cluster"],
|
|
"CLUSTER REPLICAS": [2, "cluster"],
|
|
"CLUSTER REPLICATE": [2, "cluster", "confirm"],
|
|
"CLUSTER RESET": [-2, "cluster", "confirm"],
|
|
"CLUSTER SAVECONFIG": [1, "cluster", "confirm"],
|
|
"CLUSTER SET-CONFIG-EPOCH": [2, "cluster", "confirm"],
|
|
"CLUSTER SETSLOT": [-4, "cluster", "confirm"],
|
|
"CLUSTER SHARDS": [1, "cluster"],
|
|
"CLUSTER SLAVES": [2, "cluster"],
|
|
"CLUSTER SLOTS": [1, "cluster"],
|
|
READONLY: [1, "cluster"],
|
|
READWRITE: [1, "cluster"],
|
|
};
|
|
|
|
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: safety ?? "allowed" }]));
|
|
|
|
/**
|
|
* Resolve a command spec. Handles two-token subcommands (e.g. `XGROUP CREATE`,
|
|
* `CONFIG GET`) by trying `MAIN SUB` first, then falling back to the main token.
|
|
*/
|
|
export function resolveRedisCommandSpec(argvUpper: readonly string[]): RedisCommandSpec | undefined {
|
|
if (argvUpper.length === 0) return undefined;
|
|
const main = argvUpper[0];
|
|
if (argvUpper.length >= 2) {
|
|
const sub = `${main} ${argvUpper[1]}`;
|
|
const subSpec = REDIS_COMMAND_TABLE[sub];
|
|
if (subSpec) return subSpec;
|
|
}
|
|
return REDIS_COMMAND_TABLE[main];
|
|
}
|
|
|
|
/**
|
|
* Minimal argv tokenizer for the first two tokens of a Redis command line —
|
|
* enough to resolve a spec (which only needs the command head, optionally a
|
|
* subcommand). Single-quoted, double-quoted and unquoted tokens are supported
|
|
* with backslash escapes; quoting errors are tolerated (the token still resolves).
|
|
* Kept local so this module stays self-contained (no dependency on the syntax
|
|
* diagnostics tokenizer).
|
|
*/
|
|
function firstRedisArgvUpper(line: string): string[] {
|
|
const tokens: string[] = [];
|
|
let i = 0;
|
|
const n = line.length;
|
|
while (i < n && tokens.length < 2) {
|
|
while (i < n && /\s/.test(line[i])) i++;
|
|
if (i >= n) break;
|
|
const quote = line[i] === '"' || line[i] === "'" ? line[i] : "";
|
|
let token = "";
|
|
let escaping = false;
|
|
if (quote) i++;
|
|
while (i < n) {
|
|
const ch = line[i];
|
|
if (escaping) {
|
|
token += ch;
|
|
escaping = false;
|
|
i++;
|
|
continue;
|
|
}
|
|
if (ch === "\\") {
|
|
escaping = true;
|
|
i++;
|
|
continue;
|
|
}
|
|
if (quote && ch === quote) {
|
|
i++;
|
|
break;
|
|
}
|
|
if (!quote && /\s/.test(ch)) break;
|
|
token += ch;
|
|
i++;
|
|
}
|
|
tokens.push(token.toUpperCase());
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
// Commands marked `safety: "blocked"` because they are dangerous/administrative,
|
|
// but which do NOT actually change the key set of the current db — so they should
|
|
// NOT trigger key-name completion cache invalidation. (e.g. KEYS is read-only;
|
|
// SAVE/BGSAVE/SHUTDOWN/REPLICAOF/SLAVEOF are server admin.) Everything else in the
|
|
// `blocked` set (FLUSHALL, MIGRATE, EVAL[ESHA]) may mutate keys and is left in.
|
|
const NON_MUTATING_BLOCKED = new Set(["KEYS", "BGSAVE", "SAVE", "SHUTDOWN", "REPLICAOF", "SLAVEOF"]);
|
|
|
|
/**
|
|
* True when a command may change the key set of the db it ran on (adds/edits/
|
|
* removes/renames keys, or wipes the db), and therefore the cached key-name
|
|
* completion for that db is potentially stale and should be dropped.
|
|
*
|
|
* Mapping from the diagnostic `safety` field:
|
|
* - `confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
|
|
* - `blocked` → mostly destructive/admin; we keep the ones that may touch keys
|
|
* (FLUSHALL, MIGRATE, EVAL[ESHA]) and exclude the read-only/admin
|
|
* ones in `NON_MUTATING_BLOCKED`.
|
|
* - `allowed` → read-only (GET/LRANGE/SCAN/...), never invalidates.
|
|
*
|
|
* Commands absent from the table (unknown or read-only extensions) are treated as
|
|
* non-mutating so we never thrash the cache on lookups.
|
|
*/
|
|
export function isRedisMutatingCommand(command: string): boolean {
|
|
const argv = firstRedisArgvUpper(command);
|
|
const spec = resolveRedisCommandSpec(argv);
|
|
if (!spec) return false;
|
|
if (spec.safety === "confirm") return true;
|
|
if (spec.safety === "blocked") {
|
|
return !NON_MUTATING_BLOCKED.has(argv[0]);
|
|
}
|
|
return false;
|
|
}
|