dsh-switchman 1.3.1 → 1.6.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.
@@ -2,22 +2,22 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Redis 只读查询入口
6
- * 编排流程:CLI 解析 → 命令白名单校验 → 建连 → sendCommand → 输出 → 关闭
7
- * 安全要点:db 只通过连接配置选择(不开放 SELECT 命令);
8
- * 客户端 timer 超时 destroy socket;
9
- * Redis ACL 只读用户是服务端最终边界
5
+ * Redis read-only query entry point
6
+ * Pipeline: CLI parsing → command allowlist validation → connect → sendCommand → output → close
7
+ * Security: the db is selected only via connection config (the SELECT command is not exposed);
8
+ * a client-side timer destroys the socket on timeout;
9
+ * a Redis ACL read-only user is the final server-side boundary
10
10
  */
11
11
 
12
12
  const { createClient } = require('redis');
13
13
 
14
- // ── 内部模块 ──
14
+ // ── Internal modules ──
15
15
  const { parseCli, CliError } = require('./lib/cli');
16
16
  const { loadEnvFile, resolveEnv, resolveEnvNumber, ConfigError, sanitizeTarget, safeMessage, buildTlsOptions } = require('./lib/config');
17
17
  const { validateRedisCommand, RedisReadonlyError } = require('./lib/redis-readonly');
18
18
  const { formatRedisTable, formatJson } = require('./lib/output');
19
19
 
20
- // ── CLI 参数规格 ──
20
+ // ── CLI option spec ──
21
21
  const REDIS_SPEC = {
22
22
  host: { type: 'string', default: '127.0.0.1' },
23
23
  port: { type: 'integer', default: 6379, min: 1, max: 65535 },
@@ -38,32 +38,33 @@ const REDIS_SPEC = {
38
38
 
39
39
  const ENV_PREFIX = 'DB_QUERY_REDIS_';
40
40
 
41
- // ── 主函数 ──
41
+ // ── Main ──
42
42
  async function main() {
43
43
  const { options, rest } = parseCli(process.argv.slice(2), REDIS_SPEC);
44
44
 
45
- // -- 后面必须是 Redis 命令
45
+ // A Redis command must follow --
46
46
  if (rest.length === 0) {
47
- throw new CliError('必须在 -- 之后指定 Redis 命令(如 -- GET key)');
47
+ throw new CliError('A Redis command must follow -- (e.g. -- GET key)');
48
48
  }
49
49
 
50
- // 命令白名单校验(在任何连接操作之前)
50
+ // Command allowlist validation (before any connection setup)
51
51
  const validated = validateRedisCommand(rest, {
52
52
  maxItems: options.maxItems,
53
53
  allowUnbounded: options['allow-unbounded'],
54
54
  });
55
55
 
56
- // --validate-only:纯离线校验,不建连
56
+ // --validate-only: pure offline validation, no connection
57
57
  if (options['validate-only']) {
58
- process.stderr.write(`✓ 命令校验通过: ${validated.command} ${validated.args.join(' ')}\n`);
59
- process.stderr.write(`输出形状: ${validated.shape}${validated.unbounded ? ' (无界)' : ''}\n`);
58
+ process.stderr.write(`✓ Command validation passed: ${validated.command} ${validated.args.join(' ')}\n`);
59
+ process.stderr.write(`Output shape: ${validated.shape}${validated.unbounded ? ' (unbounded)' : ''}\n`);
60
60
  return;
61
61
  }
62
62
 
63
- // env 文件加载
63
+ // Load env file
64
64
  const envPairs = loadEnvFile(options['env-file']);
65
65
 
66
- // 合并环境变量覆盖(统一用 resolveEnvNumber 做边界校验,防 env 文件绕过硬上限)
66
+ // Merge environment overrides (resolveEnvNumber uniformly enforces bounds,
67
+ // so env files cannot bypass hard limits)
67
68
  const host = resolveEnv(envPairs, ENV_PREFIX + 'HOST', options.host);
68
69
  const port = resolveEnvNumber(envPairs, ENV_PREFIX + 'PORT', options.port, { min: 1, max: 65535 });
69
70
  const user = resolveEnv(envPairs, ENV_PREFIX + 'USER', options.user);
@@ -78,11 +79,11 @@ async function main() {
78
79
  const connectTimeoutMs = resolveEnvNumber(envPairs, ENV_PREFIX + 'CONNECT_TIMEOUT_MS', options['connect-timeout-ms'], { min: 1000, max: 30000 });
79
80
  const connectTimeoutMsResolved = connectTimeoutMs;
80
81
 
81
- // 打印脱敏目标
82
+ // Print sanitized target
82
83
  const target = sanitizeTarget({ host, port, user, database: db });
83
84
  process.stderr.write(`→ Redis ${target}\n`);
84
85
 
85
- // 构建 node-redis v4 客户端
86
+ // Build the node-redis v4 client
86
87
  const clientConfig = {
87
88
  socket: {
88
89
  host,
@@ -96,22 +97,23 @@ async function main() {
96
97
  database: db,
97
98
  password: password || undefined,
98
99
  username: user,
99
- // 保留 Buffer 返回,避免自动字符串化丢二进制
100
+ // Keep Buffer results to avoid losing binary data via auto stringification
100
101
  returnBuffers: true,
101
102
  };
102
103
 
103
104
  const client = createClient(clientConfig);
104
105
 
105
- // 超时定时器(connect + sendCommand 总超时)
106
+ // Timeout timer (covers connect + sendCommand)
106
107
  const timer = setTimeout(() => {
107
108
  client.destroy();
108
- // 不直接 reject(Promise 可能已 settle),通过 error 事件处理
109
+ // Do not reject directly (the promise may already be settled); handled via the error event
109
110
  }, timeoutMsResolved);
110
111
 
111
112
  try {
112
113
  await client.connect();
113
114
 
114
- // sendCommand 以数组形式传参,绝不拼接后 split(防止参数含空格被截断)
115
+ // sendCommand takes args as an array; never join-then-split
116
+ // (prevents truncation of arguments containing spaces)
115
117
  const cmdArgs = [validated.command, ...validated.args.map(String)];
116
118
  const startMs = Date.now();
117
119
  const result = await client.sendCommand(cmdArgs);
@@ -119,7 +121,7 @@ async function main() {
119
121
 
120
122
  clearTimeout(timer);
121
123
 
122
- // 判断截断
124
+ // Detect truncation
123
125
  let count = 0;
124
126
  let truncated = false;
125
127
  if (Array.isArray(result)) {
@@ -130,14 +132,14 @@ async function main() {
130
132
  }
131
133
 
132
134
  if (options.format === 'json') {
133
- // JSON envelope 写 stdout
135
+ // JSON envelope to stdout
134
136
  process.stdout.write(JSON.stringify({
135
137
  source: 'redis',
136
138
  target,
137
139
  elapsedMs,
138
140
  count: truncated ? maxItemsResolved : count,
139
141
  truncated,
140
- // Buffer → base64 以确保 JSON 安全
142
+ // Buffer → base64 to keep the JSON safe
141
143
  result: sanitizeForJson(result),
142
144
  }, null, 2) + '\n');
143
145
  } else {
@@ -149,16 +151,16 @@ async function main() {
149
151
  }) + '\n');
150
152
  }
151
153
 
152
- process.stderr.write(`✓ ${count} 条${truncated ? '(已截断)' : ''},${elapsedMs}ms\n`);
154
+ process.stderr.write(`✓ ${count} item(s)${truncated ? ' (truncated)' : ''} in ${elapsedMs}ms\n`);
153
155
  } catch (e) {
154
156
  clearTimeout(timer);
155
157
  throw e;
156
158
  } finally {
157
- try { await client.quit(); } catch (_) { /* 忽略关闭错误 */ }
159
+ try { await client.quit(); } catch (_) { /* ignore close errors */ }
158
160
  }
159
161
  }
160
162
 
161
- /** 递归将 Buffer 转为 base64,确保 JSON 安全 */
163
+ /** Recursively convert Buffers to base64 to keep the JSON safe */
162
164
  function sanitizeForJson(val) {
163
165
  if (Buffer.isBuffer(val)) return val.toString('base64');
164
166
  if (Array.isArray(val)) return val.map(sanitizeForJson);
@@ -171,7 +173,8 @@ function sanitizeForJson(val) {
171
173
  }
172
174
 
173
175
  main().catch(e => {
174
- // 校验类/配置类错误直接显示消息(不是连接错误,不需要脱敏)
176
+ // Validation/config errors show their message directly (not connection
177
+ // errors, no sanitizing needed)
175
178
  if (e instanceof CliError || e instanceof RedisReadonlyError || e instanceof ConfigError) {
176
179
  process.stderr.write('✗ ' + e.message + '\n');
177
180
  } else {
@@ -1,51 +1,51 @@
1
1
  #!/usr/bin/env bash
2
- # db-query 技能一键环境检查 + 安装 + 测试
3
- # 幂等:依赖已装则跳过 npm ci
2
+ # db-query skill: one-shot environment check + install + test
3
+ # Idempotent: skip npm ci when dependencies are already installed
4
4
 
5
5
  set -euo pipefail
6
6
 
7
7
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8
8
  cd "$SCRIPT_DIR"
9
9
 
10
- # ── Node / npm 版本检查 ──
10
+ # ── Node / npm version check ──
11
11
  if ! command -v node &>/dev/null; then
12
- echo "✗ 未找到 node,需要 Node.js >= 18"
12
+ echo "✗ node not found; Node.js >= 18 is required"
13
13
  exit 1
14
14
  fi
15
15
 
16
16
  NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
17
17
  if [ "$NODE_MAJOR" -lt 18 ]; then
18
- echo "✗ Node.js 版本 $(node -v) 低于最低要求 18"
18
+ echo "✗ Node.js version $(node -v) is below the minimum required 18"
19
19
  exit 1
20
20
  fi
21
21
 
22
22
  if ! command -v npm &>/dev/null; then
23
- echo "✗ 未找到 npm"
23
+ echo "✗ npm not found"
24
24
  exit 1
25
25
  fi
26
26
 
27
27
  echo "Node $(node -v) ✓ npm $(npm -v) ✓"
28
28
 
29
- # ── 幂等安装运行时依赖 ──
30
- # 已有 node_modules 时 npm ci 仍会检查一致性,幂等安全
29
+ # ── Idempotent install of runtime dependencies ──
30
+ # When node_modules already exists, npm ci would still verify consistency, so skipping is idempotent-safe
31
31
  if [ -d node_modules ]; then
32
- echo "依赖已存在,跳过安装(删除 node_modules 可强制重装)"
32
+ echo "Dependencies already present, skipping install (delete node_modules to force a reinstall)"
33
33
  else
34
- echo "安装运行时依赖(mysql2 redis dotenv)..."
34
+ echo "Installing runtime dependencies (mysql2 redis dotenv)..."
35
35
  if [ -f package-lock.json ]; then
36
36
  npm ci --omit=dev --no-audit --no-fund
37
37
  else
38
38
  npm install --omit=dev --no-audit --no-fund
39
39
  fi
40
- echo "依赖安装 ✓"
40
+ echo "Dependencies installed ✓"
41
41
  fi
42
42
 
43
- # ── 运行测试 ──
44
- echo "运行测试..."
43
+ # ── Run tests ──
44
+ echo "Running tests..."
45
45
  if node --test tests/*.test.js 2>/dev/null; then
46
- echo "全部测试通过 ✓"
46
+ echo "All tests passed ✓"
47
47
  else
48
- echo "⚠ 未找到测试文件或测试未通过(首次部署无测试属正常)"
48
+ echo "⚠ No test files found or tests did not pass (no tests on first deployment is normal)"
49
49
  fi
50
50
 
51
- echo "db-query 环境就绪 ✓"
51
+ echo "db-query environment ready ✓"