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.
@@ -1,18 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * 结果输出:表格/JSON 格式化 + 类型规范化 + 截断保护
5
- * JSON 写 stdout、状态信息写 stderr,保证 stdout 是合法 JSON
4
+ * Result output: table/JSON formatting + value normalization + truncation guards
5
+ * JSON goes to stdout and status to stderr so stdout stays valid JSON
6
6
  */
7
7
 
8
- // ── 类型规范化 ──
8
+ // ── Value normalization ──
9
9
 
10
10
  /**
11
- * 规范化单个值为安全输出格式
12
- * - BigInt → 十进制字符串
13
- * - Buffer/Uint8Array → UTF-8 可打印则字符串,否则 base64
14
- * - Date → ISO 字符串
15
- * - 其他对象 → 安全 JSON 化
11
+ * Normalize a single value into a safe output format
12
+ * - BigInt → decimal string
13
+ * - Buffer/Uint8Array → string if printable UTF-8, otherwise base64
14
+ * - Date → ISO string
15
+ * - other objects → safe JSON
16
16
  *
17
17
  * @param {*} val
18
18
  * @param {object} opts - { maxValueBytes: number }
@@ -21,22 +21,23 @@
21
21
  function normalizeValue(val, opts) {
22
22
  const maxSize = (opts && opts.maxValueBytes) || 4096;
23
23
 
24
- // BigInt 保留字符串精度
24
+ // BigInt keeps string precision
25
25
  if (typeof val === 'bigint') {
26
26
  return { value: val.toString(10), truncated: false };
27
27
  }
28
28
 
29
- // Buffer / Uint8Array 按二进制处理
29
+ // Treat Buffer / Uint8Array as binary
30
30
  if (Buffer.isBuffer(val) || (val instanceof Uint8Array)) {
31
31
  return normalizeBuffer(val, maxSize);
32
32
  }
33
33
 
34
- // Date → ISO 字符串(dateStrings 已在连接选项中处理,这里兜底)
34
+ // Date → ISO string (dateStrings already handles this in the connection
35
+ // options; fallback here)
35
36
  if (val instanceof Date) {
36
37
  return { value: val.toISOString(), truncated: false };
37
38
  }
38
39
 
39
- // 对象(含数组)→ JSON 安全化
40
+ // Objects (including arrays) → JSON-safe
40
41
  if (typeof val === 'object' && val !== null) {
41
42
  try {
42
43
  const json = safeJsonStringify(val);
@@ -50,19 +51,19 @@ function normalizeValue(val, opts) {
50
51
  }
51
52
  }
52
53
 
53
- // 基本类型直接返回
54
+ // Primitives pass through
54
55
  return { value: val, truncated: false };
55
56
  }
56
57
 
57
58
  /**
58
- * Buffer/Uint8Array → 可打印 UTF-8 字符串或 base64
59
- * 超过 maxValueBytes 时截断并标注
59
+ * Buffer/Uint8Array → printable UTF-8 string or base64
60
+ * Truncate and mark when larger than maxValueBytes
60
61
  */
61
62
  function normalizeBuffer(buf, maxSize) {
62
- // Uint8Array → Buffer(共享底层内存,零拷贝视图)
63
+ // Uint8Array → Buffer (zero-copy view over the same memory)
63
64
  const buffer = Buffer.isBuffer(buf) ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
64
65
 
65
- // 尝试 UTF-8 解码
66
+ // Try UTF-8 decoding
66
67
  let str;
67
68
  let isPrintable = false;
68
69
  try {
@@ -77,7 +78,7 @@ function normalizeBuffer(buf, maxSize) {
77
78
 
78
79
  if (byteLength > maxSize) {
79
80
  truncated = true;
80
- // 截断到 maxSize 字节再解码
81
+ // Truncate to maxSize bytes, then decode
81
82
  const sliced = buffer.subarray(0, maxSize);
82
83
  if (isPrintable) {
83
84
  str = sliced.toString('utf8') + '…';
@@ -85,7 +86,7 @@ function normalizeBuffer(buf, maxSize) {
85
86
  str = sliced.toString('base64') + '…';
86
87
  }
87
88
  } else if (isPrintable) {
88
- str = str; // 已经解码
89
+ str = str; // already decoded
89
90
  } else {
90
91
  str = buffer.toString('base64');
91
92
  }
@@ -97,19 +98,19 @@ function normalizeBuffer(buf, maxSize) {
97
98
  };
98
99
  }
99
100
 
100
- /** 检查字符串是否为可打印 UTF-8(无控制字符,允许常见空白) */
101
+ /** Check whether a string is printable UTF-8 (no control characters; common whitespace allowed) */
101
102
  function isPrintableUtf8(str) {
102
103
  for (let i = 0; i < str.length; i++) {
103
104
  const code = str.charCodeAt(i);
104
105
  if (code < 0x20 && code !== 0x09 && code !== 0x0A && code !== 0x0D) return false;
105
106
  if (code === 0x7F) return false; // DEL
106
- if (code >= 0xFFFE) return false; // 非字符
107
+ if (code >= 0xFFFE) return false; // noncharacter
107
108
  }
108
109
  return true;
109
110
  }
110
111
 
111
112
  /**
112
- * 安全 JSON 字符串化,处理循环引用等异常
113
+ * Safe JSON.stringify that handles circular references and similar pitfalls
113
114
  */
114
115
  function safeJsonStringify(val) {
115
116
  const seen = new WeakSet();
@@ -122,22 +123,22 @@ function safeJsonStringify(val) {
122
123
  });
123
124
  }
124
125
 
125
- /** 截断字符串到指定字节数(UTF-8 安全) */
126
+ /** Truncate a string to a byte budget (UTF-8 safe) */
126
127
  function truncateString(str, maxBytes) {
127
128
  const buf = Buffer.from(str, 'utf8');
128
129
  if (buf.length <= maxBytes) return { value: str, truncated: false };
129
- // 找到不超过 maxBytes 的最后完整 UTF-8 字符边界
130
+ // Find the last complete UTF-8 character boundary not exceeding maxBytes
130
131
  let end = maxBytes;
131
- while (end > 0 && (buf[end] & 0xC0) === 0x80) end--; // 跳过多字节续字节
132
+ while (end > 0 && (buf[end] & 0xC0) === 0x80) end--; // skip multibyte continuation bytes
132
133
  return { value: buf.subarray(0, end).toString('utf8') + '…', truncated: true };
133
134
  }
134
135
 
135
- // ── MySQL 表格输出 ──
136
+ // ── MySQL table output ──
136
137
 
137
138
  /**
138
- * 格式化 MySQL 查询结果为表格(按 fields 顺序)
139
+ * Format MySQL query results as a table (in field order)
139
140
  * @param {Array} rows
140
- * @param {Array} fields - mysql2 返回的 field metadata
141
+ * @param {Array} fields - field metadata returned by mysql2
141
142
  * @param {object} opts - { maxRows, maxValueBytes, truncated }
142
143
  */
143
144
  function formatMysqlTable(rows, fields, opts) {
@@ -145,7 +146,7 @@ function formatMysqlTable(rows, fields, opts) {
145
146
  const maxValueBytes = (opts && opts.maxValueBytes) || 4096;
146
147
  const resultTruncated = opts && opts.truncated;
147
148
 
148
- // 规范化行数据
149
+ // Normalize row data
149
150
  const normalizedRows = [];
150
151
  const fieldNames = (fields || []).map(f => f.name);
151
152
 
@@ -161,27 +162,27 @@ function formatMysqlTable(rows, fields, opts) {
161
162
  }
162
163
 
163
164
  if (normalizedRows.length === 0) {
164
- return '(空结果集)';
165
+ return '(empty result set)';
165
166
  }
166
167
 
167
- // 计算列宽
168
+ // Compute column widths
168
169
  const colWidths = fieldNames.map((name, colIdx) => {
169
170
  let w = name.length;
170
171
  for (const row of normalizedRows) {
171
172
  w = Math.max(w, String(row[colIdx]).length);
172
173
  }
173
- return Math.min(w, 60); // 单列最大 60 字符
174
+ return Math.min(w, 60); // cap each column at 60 chars
174
175
  });
175
176
 
176
- // 构建表格
177
+ // Build the table
177
178
  const sep = colWidths.map(w => '-'.repeat(w + 2)).join('+');
178
179
  const lines = [sep];
179
180
 
180
- // 表头
181
+ // Header
181
182
  lines.push('|' + fieldNames.map((name, i) => ' ' + name.padEnd(colWidths[i]) + ' ').join('|') + '|');
182
183
  lines.push(sep);
183
184
 
184
- // 数据行
185
+ // Data rows
185
186
  for (const row of normalizedRows) {
186
187
  lines.push('|' + row.map((cell, i) => ' ' + String(cell).padEnd(colWidths[i]) + ' ').join('|') + '|');
187
188
  }
@@ -189,27 +190,27 @@ function formatMysqlTable(rows, fields, opts) {
189
190
 
190
191
  let output = lines.join('\n');
191
192
  if (resultTruncated) {
192
- output += `\n⚠ 结果已截断(仅显示前 ${maxRows} 行)`;
193
+ output += `\n⚠ Results truncated (showing first ${maxRows} rows)`;
193
194
  }
194
195
  return output;
195
196
  }
196
197
 
197
- /** 格式化单个单元格(附加截断标记) */
198
+ /** Format a single cell (append truncation marker) */
198
199
  function formatCell(norm) {
199
200
  if (norm.truncated && norm.byteLength) {
200
201
  return String(norm.value) + ` [${norm.byteLength}B]`;
201
202
  }
202
203
  if (norm.truncated) {
203
- return String(norm.value) + ' [截断]';
204
+ return String(norm.value) + ' [truncated]';
204
205
  }
205
206
  return norm.value;
206
207
  }
207
208
 
208
- // ── Redis 表格输出 ──
209
+ // ── Redis table output ──
209
210
 
210
211
  /**
211
- * 格式化 Redis 结果为可读表格
212
- * @param {*} result - Redis 返回的原始数据
212
+ * Format a Redis result as a readable table
213
+ * @param {*} result - raw data returned by Redis
213
214
  * @param {object} info - { shape, command, maxItems, maxValueBytes }
214
215
  * @returns {string}
215
216
  */
@@ -222,36 +223,36 @@ function formatRedisTable(result, info) {
222
223
  if (result === null) return '(nil)';
223
224
  if (typeof result === 'number') return String(result);
224
225
  const norm = normalizeValue(result, { maxValueBytes: info && info.maxValueBytes });
225
- return String(norm.value) + (norm.truncated ? ' [截断]' : '');
226
+ return String(norm.value) + (norm.truncated ? ' [truncated]' : '');
226
227
  }
227
228
 
228
229
  // list
229
230
  if (shape === 'list') {
230
231
  const items = Array.isArray(result) ? result : [result];
231
- if (items.length === 0) return '(空列表)';
232
+ if (items.length === 0) return '(empty list)';
232
233
  const lines = [];
233
234
  const count = Math.min(items.length, maxItems);
234
235
  for (let i = 0; i < count; i++) {
235
236
  const norm = normalizeValue(items[i], { maxValueBytes: info && info.maxValueBytes });
236
- lines.push(`${i + 1}) ${String(norm.value)}${norm.truncated ? ' [截断]' : ''}`);
237
+ lines.push(`${i + 1}) ${String(norm.value)}${norm.truncated ? ' [truncated]' : ''}`);
237
238
  }
238
- if (items.length > maxItems) lines.push(`⚠ 仅显示前 ${maxItems} 条(共 ${items.length} 条)`);
239
+ if (items.length > maxItems) lines.push(`⚠ Showing first ${maxItems} of ${items.length} item(s)`);
239
240
  return lines.join('\n');
240
241
  }
241
242
 
242
- // pairs (HGETALL 等)
243
+ // pairs (HGETALL etc.)
243
244
  if (shape === 'pairs') {
244
245
  const arr = Array.isArray(result) ? result : [];
245
- if (arr.length === 0) return '(空映射)';
246
+ if (arr.length === 0) return '(empty map)';
246
247
  const lines = [];
247
248
  const pairCount = Math.min(Math.floor(arr.length / 2), maxItems);
248
249
  for (let i = 0; i < pairCount; i++) {
249
250
  const field = String(arr[i * 2] || '');
250
251
  const val = normalizeValue(arr[i * 2 + 1], { maxValueBytes: info && info.maxValueBytes });
251
- lines.push(`${field}: ${String(val.value)}${val.truncated ? ' [截断]' : ''}`);
252
+ lines.push(`${field}: ${String(val.value)}${val.truncated ? ' [truncated]' : ''}`);
252
253
  }
253
254
  if (Math.floor(arr.length / 2) > maxItems) {
254
- lines.push(`⚠ 仅显示前 ${maxItems} 个字段`);
255
+ lines.push(`⚠ Showing first ${maxItems} field(s)`);
255
256
  }
256
257
  return lines.join('\n');
257
258
  }
@@ -266,7 +267,7 @@ function formatRedisTable(result, info) {
266
267
  for (let i = 0; i < count; i++) {
267
268
  lines.push(`${i + 1}) ${String(items[i])}`);
268
269
  }
269
- if (items.length > maxItems) lines.push(`⚠ 仅显示前 ${maxItems} 条`);
270
+ if (items.length > maxItems) lines.push(`⚠ Showing first ${maxItems} item(s)`);
270
271
  return lines.join('\n');
271
272
  }
272
273
 
@@ -282,10 +283,10 @@ function formatRedisTable(result, info) {
282
283
  const count = Math.min(clients.length, maxItems);
283
284
  const lines = [];
284
285
  for (let i = 0; i < count; i++) {
285
- lines.push(`--- 客户端 ${i + 1} ---`);
286
+ lines.push(`--- Client ${i + 1} ---`);
286
287
  lines.push(clients[i]);
287
288
  }
288
- if (clients.length > maxItems) lines.push(`⚠ 仅显示前 ${maxItems} 个客户端`);
289
+ if (clients.length > maxItems) lines.push(`⚠ Showing first ${maxItems} client(s)`);
289
290
  return lines.join('\n');
290
291
  }
291
292
  return String(result);
@@ -297,16 +298,16 @@ function formatRedisTable(result, info) {
297
298
  // ── JSON envelope ──
298
299
 
299
300
  /**
300
- * 构建 JSON 输出 envelope
301
+ * Build the JSON output envelope
301
302
  * @param {object} params - { source, target, elapsedMs, count, truncated, result }
302
303
  * @returns {string}
303
304
  */
304
305
  function formatJson(params) {
305
306
  const envelope = {
306
307
  source: params.source, // 'mysql' | 'redis'
307
- target: params.target, // 脱敏目标
308
+ target: params.target, // sanitized target
308
309
  elapsedMs: params.elapsedMs,
309
- count: params.count, // 行/元素数
310
+ count: params.count, // row/item count
310
311
  truncated: params.truncated,
311
312
  result: params.result,
312
313
  };