kami-redis 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ // src/index.ts
2
+ import meow from "meow";
3
+ import pc2 from "picocolors";
4
+ import { createClient } from "redis";
5
+
6
+ // src/utils/clean-up.ts
7
+ function cleanup() {
8
+ return console.clear();
9
+ }
10
+
11
+ // src/utils/graceful-exit.ts
12
+ async function gracefulExit(code = 0) {
13
+ try {
14
+ if (process.stdin.isTTY) {
15
+ try {
16
+ process.stdin.setRawMode(false);
17
+ } catch {
18
+ }
19
+ }
20
+ try {
21
+ process.stdin.pause();
22
+ } catch {
23
+ await client.quit();
24
+ }
25
+ } catch {
26
+ } finally {
27
+ cleanup();
28
+ process.exit(code);
29
+ }
30
+ }
31
+
32
+ // src/utils/is-redis-url.ts
33
+ function isRedisUrl(url2) {
34
+ let parsed;
35
+ try {
36
+ parsed = new URL(url2);
37
+ } catch {
38
+ return false;
39
+ }
40
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
41
+ return false;
42
+ }
43
+ if (!parsed.hostname) {
44
+ return false;
45
+ }
46
+ if (parsed.port && Number.isNaN(Number(parsed.port))) {
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+
52
+ // src/utils/prompt-for-key.ts
53
+ import inquirer from "inquirer";
54
+ async function promptForKey() {
55
+ const keys = await client.keys("*");
56
+ if (keys.length === 0)
57
+ return null;
58
+ try {
59
+ const { selectedKey } = await inquirer.prompt({
60
+ type: "list",
61
+ name: "selectedKey",
62
+ message: "Select a key to view its value:",
63
+ choices: keys,
64
+ pageSize: 20,
65
+ theme: {
66
+ icon: {
67
+ cursor: ">"
68
+ },
69
+ prefix: "0_0"
70
+ }
71
+ });
72
+ return selectedKey;
73
+ } catch {
74
+ await gracefulExit(0);
75
+ }
76
+ }
77
+
78
+ // src/utils/view-key.ts
79
+ import boxen from "boxen";
80
+ import highlight from "cli-highlight";
81
+ import clipboardy from "clipboardy";
82
+ import readline from "readline";
83
+ import pc from "picocolors";
84
+
85
+ // src/utils/format-value.ts
86
+ function formatValue(value) {
87
+ if (typeof value === "object" && value !== null) {
88
+ return JSON.stringify(value, null, 2);
89
+ }
90
+ if (typeof value === "string") {
91
+ try {
92
+ return JSON.stringify(JSON.parse(value), null, 2);
93
+ } catch {
94
+ return value;
95
+ }
96
+ }
97
+ return String(value);
98
+ }
99
+
100
+ // src/utils/get-value.ts
101
+ async function getValue(selectedKey, type) {
102
+ switch (type) {
103
+ case "string":
104
+ return client.get(selectedKey);
105
+ case "hash":
106
+ return client.hGetAll(selectedKey);
107
+ case "list":
108
+ return client.lRange(selectedKey, 0, -1);
109
+ case "set":
110
+ return client.sMembers(selectedKey);
111
+ case "zset":
112
+ return client.zRangeWithScores(selectedKey, 0, -1);
113
+ default:
114
+ return "(unsupported type)";
115
+ }
116
+ }
117
+
118
+ // src/utils/view-key.ts
119
+ async function viewKey(selectedKey) {
120
+ const type = await client.type(selectedKey);
121
+ const value = await getValue(selectedKey, type);
122
+ cleanup();
123
+ console.log(
124
+ boxen(highlight(formatValue(value), { language: "json", ignoreIllegals: true }), {
125
+ borderColor: "cyan",
126
+ padding: 1,
127
+ title: selectedKey,
128
+ borderStyle: "round"
129
+ })
130
+ );
131
+ console.log(pc.blue("Press 'd' to delete, 'c' to copy, \u2190 to go back, Ctrl+C to quit."));
132
+ readline.emitKeypressEvents(process.stdin);
133
+ try {
134
+ process.stdin.resume();
135
+ } catch {
136
+ }
137
+ if (process.stdin.isTTY) {
138
+ process.stdin.setRawMode(true);
139
+ }
140
+ return await new Promise((resolve) => {
141
+ const onKeypress = async (str, key) => {
142
+ if (!key) {
143
+ return;
144
+ }
145
+ if (key.name === "d" && !key.ctrl && !key.meta) {
146
+ try {
147
+ await client.del(selectedKey);
148
+ } catch {
149
+ console.error(pc.red("Delete failed:"));
150
+ }
151
+ cleanup();
152
+ resolve("deleted");
153
+ return;
154
+ }
155
+ if (key.name === "c" && !key.ctrl && !key.meta) {
156
+ try {
157
+ clipboardy.writeSync(formatValue(value));
158
+ cleanup();
159
+ resolve("back");
160
+ } catch (e) {
161
+ console.error(pc.red("Copy failed:"), e);
162
+ }
163
+ return;
164
+ }
165
+ if (key.name === "left") {
166
+ cleanup();
167
+ resolve("back");
168
+ return;
169
+ }
170
+ if (key.ctrl && key.name === "c" || key.sequence === "") {
171
+ cleanup();
172
+ resolve("quit");
173
+ }
174
+ };
175
+ const onData = async (chunk) => {
176
+ const s = chunk.toString();
177
+ if (s.includes("")) {
178
+ cleanup();
179
+ resolve("quit");
180
+ }
181
+ };
182
+ process.stdin.on("keypress", onKeypress);
183
+ process.stdin.on("data", onData);
184
+ });
185
+ }
186
+
187
+ // src/index.ts
188
+ var cli = meow(
189
+ `
190
+ Usage
191
+ foo <url>
192
+ `,
193
+ {
194
+ importMeta: import.meta
195
+ }
196
+ );
197
+ var [url] = cli.input;
198
+ if (!url) {
199
+ console.log(cli.help);
200
+ process.exit(1);
201
+ }
202
+ if (!isRedisUrl(url)) {
203
+ console.log(cli.help);
204
+ console.log(pc2.red("Provided URL is not a valid Redis URL"));
205
+ process.exit(1);
206
+ }
207
+ var client = createClient({ url });
208
+ async function main() {
209
+ try {
210
+ cleanup();
211
+ await client.connect();
212
+ while (true) {
213
+ const selectedKey = await promptForKey();
214
+ if (!selectedKey) {
215
+ console.log("No keys in Redis!");
216
+ await client.quit();
217
+ process.exit(0);
218
+ }
219
+ const action = await viewKey(selectedKey);
220
+ if (action === "quit") {
221
+ await gracefulExit(0);
222
+ } else if (action === "deleted") {
223
+ continue;
224
+ } else if (action === "back") {
225
+ continue;
226
+ }
227
+ }
228
+ } catch (err) {
229
+ console.error(pc2.red("Unexpected error:"), err);
230
+ try {
231
+ await client.quit();
232
+ } catch {
233
+ process.exit(1);
234
+ }
235
+ }
236
+ }
237
+ main();
238
+ export {
239
+ client
240
+ };
@@ -0,0 +1,28 @@
1
+ import antfu from "@antfu/eslint-config";
2
+
3
+ export default antfu({
4
+ type: "app",
5
+ typescript: true,
6
+ formatters: true,
7
+ stylistic: {
8
+ indent: 2,
9
+ semi: true,
10
+ quotes: "double",
11
+ },
12
+ }, {
13
+ rules: {
14
+ "ts/no-redeclare": "off",
15
+ "ts/consistent-type-definitions": ["error", "type"],
16
+ "no-console": ["off"],
17
+ "antfu/no-top-level-await": ["off"],
18
+ "node/prefer-global/process": ["off"],
19
+ "node/no-process-env": ["off"],
20
+ "perfectionist/sort-imports": ["error", {
21
+ tsconfigRootDir: ".",
22
+ }],
23
+ "unicorn/filename-case": ["error", {
24
+ case: "kebabCase",
25
+ ignore: ["README.md"],
26
+ }],
27
+ },
28
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "kami-redis",
3
+ "type": "module",
4
+ "version": "2.0.0",
5
+ "description": "Browse, inspect, copy, and delete Redis keys from your CLI",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "dev": "bun run --watch src/index.ts",
12
+ "build": "tsup src/index.ts",
13
+ "lint": "eslint",
14
+ "lint:fix": "eslint --fix",
15
+ "release": "bun run build && changeset publish",
16
+ "change": "changeset"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "^5"
20
+ },
21
+ "dependencies": {
22
+ "boxen": "^8.0.1",
23
+ "cli-highlight": "^2.1.11",
24
+ "clipboardy": "^5.0.0",
25
+ "inquirer": "^12.9.6",
26
+ "meow": "^14.0.0",
27
+ "picocolors": "^1.1.1",
28
+ "redis": "^5.8.2"
29
+ },
30
+ "devDependencies": {
31
+ "@antfu/eslint-config": "^5.4.1",
32
+ "@changesets/cli": "^2.29.7",
33
+ "@types/bun": "latest",
34
+ "eslint": "^9.36.0",
35
+ "eslint-plugin-format": "^1.0.2",
36
+ "tsup": "^8.5.0"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ /* eslint-disable style/no-tabs */
2
+ import meow from "meow";
3
+ import pc from "picocolors";
4
+ import { createClient } from "redis";
5
+
6
+ import { cleanup } from "./utils/clean-up";
7
+ import { gracefulExit } from "./utils/graceful-exit";
8
+ import { isRedisUrl } from "./utils/is-redis-url";
9
+ import { promptForKey } from "./utils/prompt-for-key";
10
+ import { viewKey } from "./utils/view-key";
11
+
12
+ const cli = meow(
13
+ `
14
+ Usage
15
+ foo <url>
16
+ `,
17
+ {
18
+ importMeta: import.meta,
19
+ },
20
+ );
21
+
22
+ const [url] = cli.input;
23
+
24
+ if (!url) {
25
+ console.log(cli.help);
26
+ process.exit(1);
27
+ }
28
+
29
+ if (!isRedisUrl(url)) {
30
+ console.log(cli.help);
31
+ console.log(pc.red("Provided URL is not a valid Redis URL"));
32
+ process.exit(1);
33
+ }
34
+
35
+ export const client = createClient({ url });
36
+
37
+ async function main() {
38
+ try {
39
+ cleanup();
40
+ await client.connect();
41
+
42
+ while (true) {
43
+ const selectedKey = await promptForKey();
44
+ if (!selectedKey) {
45
+ console.log("No keys in Redis!");
46
+ await client.quit();
47
+ process.exit(0);
48
+ }
49
+
50
+ const action = await viewKey(selectedKey);
51
+
52
+ if (action === "quit") {
53
+ await gracefulExit(0);
54
+ }
55
+ else if (action === "deleted") {
56
+ continue;
57
+ }
58
+ else if (action === "back") {
59
+ continue;
60
+ }
61
+ }
62
+ }
63
+ catch (err) {
64
+ console.error(pc.red("Unexpected error:"), err);
65
+ try {
66
+ await client.quit();
67
+ }
68
+ catch {
69
+ process.exit(1);
70
+ }
71
+ }
72
+ }
73
+
74
+ main();
@@ -0,0 +1,3 @@
1
+ export function cleanup() {
2
+ return console.clear();
3
+ }
@@ -0,0 +1,16 @@
1
+ export function formatValue(value: unknown): string {
2
+ if (typeof value === "object" && value !== null) {
3
+ return JSON.stringify(value, null, 2);
4
+ }
5
+
6
+ if (typeof value === "string") {
7
+ try {
8
+ return JSON.stringify(JSON.parse(value), null, 2);
9
+ }
10
+ catch {
11
+ return value;
12
+ }
13
+ }
14
+
15
+ return String(value);
16
+ }
@@ -0,0 +1,18 @@
1
+ import { client } from "..";
2
+
3
+ export async function getValue(selectedKey: string, type: string): Promise<unknown> {
4
+ switch (type) {
5
+ case "string":
6
+ return client.get(selectedKey);
7
+ case "hash":
8
+ return client.hGetAll(selectedKey);
9
+ case "list":
10
+ return client.lRange(selectedKey, 0, -1);
11
+ case "set":
12
+ return client.sMembers(selectedKey);
13
+ case "zset":
14
+ return client.zRangeWithScores(selectedKey, 0, -1);
15
+ default:
16
+ return "(unsupported type)";
17
+ }
18
+ }
@@ -0,0 +1,24 @@
1
+ import { client } from "..";
2
+ import { cleanup } from "./clean-up";
3
+
4
+ export async function gracefulExit(code = 0) {
5
+ try {
6
+ if (process.stdin.isTTY) {
7
+ try {
8
+ process.stdin.setRawMode(false);
9
+ }
10
+ catch {}
11
+ }
12
+ try {
13
+ process.stdin.pause();
14
+ }
15
+ catch {
16
+ await client.quit();
17
+ }
18
+ }
19
+ catch {}
20
+ finally {
21
+ cleanup();
22
+ process.exit(code);
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ export function isRedisUrl(url: string): boolean {
2
+ let parsed;
3
+ try {
4
+ parsed = new URL(url);
5
+ }
6
+ catch {
7
+ return false;
8
+ }
9
+
10
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
11
+ return false;
12
+ }
13
+
14
+ if (!parsed.hostname) {
15
+ return false;
16
+ }
17
+
18
+ if (parsed.port && Number.isNaN(Number(parsed.port))) {
19
+ return false;
20
+ }
21
+
22
+ return true;
23
+ }
@@ -0,0 +1,30 @@
1
+ import inquirer from "inquirer";
2
+
3
+ import { client } from "..";
4
+ import { gracefulExit } from "./graceful-exit";
5
+
6
+ export async function promptForKey() {
7
+ const keys = await client.keys("*");
8
+ if (keys.length === 0)
9
+ return null;
10
+
11
+ try {
12
+ const { selectedKey } = await inquirer.prompt({
13
+ type: "list",
14
+ name: "selectedKey",
15
+ message: "Select a key to view its value:",
16
+ choices: keys,
17
+ pageSize: 20,
18
+ theme: {
19
+ icon: {
20
+ cursor: ">",
21
+ },
22
+ prefix: "0_0",
23
+ },
24
+ });
25
+ return selectedKey;
26
+ }
27
+ catch {
28
+ await gracefulExit(0);
29
+ }
30
+ }
@@ -0,0 +1,95 @@
1
+ import type { Buffer } from "node:buffer";
2
+
3
+ import boxen from "boxen";
4
+ import highlight from "cli-highlight";
5
+ import clipboardy from "clipboardy";
6
+ import readline from "node:readline";
7
+ import pc from "picocolors";
8
+
9
+ import { client } from "..";
10
+ import { cleanup } from "./clean-up";
11
+ import { formatValue } from "./format-value";
12
+ import { getValue } from "./get-value";
13
+
14
+ export async function viewKey(selectedKey: string): Promise<"back" | "deleted" | "quit"> {
15
+ const type = await client.type(selectedKey);
16
+
17
+ const value = await getValue(selectedKey, type);
18
+
19
+ cleanup();
20
+
21
+ console.log(
22
+ boxen(highlight(formatValue(value), { language: "json", ignoreIllegals: true }), {
23
+ borderColor: "cyan",
24
+ padding: 1,
25
+ title: selectedKey,
26
+ borderStyle: "round",
27
+ }),
28
+ );
29
+ console.log(pc.blue("Press 'd' to delete, 'c' to copy, ← to go back, Ctrl+C to quit."));
30
+
31
+ readline.emitKeypressEvents(process.stdin);
32
+
33
+ try {
34
+ process.stdin.resume();
35
+ }
36
+
37
+ catch {}
38
+ if (process.stdin.isTTY) {
39
+ process.stdin.setRawMode(true);
40
+ }
41
+
42
+ return await new Promise((resolve) => {
43
+ const onKeypress = async (str: string, key: readline.Key) => {
44
+ if (!key) {
45
+ return;
46
+ }
47
+
48
+ if (key.name === "d" && !key.ctrl && !key.meta) {
49
+ try {
50
+ await client.del(selectedKey);
51
+ }
52
+ catch {
53
+ console.error(pc.red("Delete failed:"));
54
+ }
55
+ cleanup();
56
+ resolve("deleted");
57
+ return;
58
+ }
59
+
60
+ if (key.name === "c" && !key.ctrl && !key.meta) {
61
+ try {
62
+ clipboardy.writeSync(formatValue(value));
63
+ cleanup();
64
+ resolve("back");
65
+ }
66
+ catch (e) {
67
+ console.error(pc.red("Copy failed:"), e);
68
+ }
69
+ return;
70
+ }
71
+
72
+ if (key.name === "left") {
73
+ cleanup();
74
+ resolve("back");
75
+ return;
76
+ }
77
+
78
+ if ((key.ctrl && key.name === "c") || key.sequence === "\u0003") {
79
+ cleanup();
80
+ resolve("quit");
81
+ }
82
+ };
83
+
84
+ const onData = async (chunk: Buffer | string) => {
85
+ const s = chunk.toString();
86
+ if (s.includes("\u0003")) {
87
+ cleanup();
88
+ resolve("quit");
89
+ }
90
+ };
91
+
92
+ process.stdin.on("keypress", onKeypress);
93
+ process.stdin.on("data", onData);
94
+ });
95
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "jsx": "react-jsx",
5
+
6
+ "lib": ["ESNext"],
7
+ "moduleDetection": "force",
8
+ "module": "Preserve",
9
+
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "allowJs": true,
13
+
14
+ "strict": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noImplicitOverride": true,
17
+
18
+ "noPropertyAccessFromIndexSignature": false,
19
+ "noUncheckedIndexedAccess": true,
20
+
21
+ "noUnusedLocals": false,
22
+ "noUnusedParameters": false,
23
+ "noEmit": true,
24
+ "verbatimModuleSyntax": true,
25
+ "skipLibCheck": true
26
+ }
27
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ outDir: "dist",
6
+ clean: true,
7
+ format: ["esm"],
8
+ dts: true,
9
+ });