dsh-switchman 1.3.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/LICENSE +21 -0
- package/README.md +79 -0
- package/README.zh.md +79 -0
- package/client.js +2441 -0
- package/cordis.patch.yml +599 -0
- package/host/config.js +116 -0
- package/host/context-watch.js +620 -0
- package/host/data/capability-default.json +1097 -0
- package/host/dispatch.js +205 -0
- package/host/lang.js +182 -0
- package/host/lib/rank.js +187 -0
- package/host/routes.js +398 -0
- package/icon.svg +7 -0
- package/index.js +154 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +70 -0
- package/skills/db-query/SKILL.md +322 -0
- package/skills/db-query/package-lock.json +251 -0
- package/skills/db-query/package.json +20 -0
- package/skills/db-query/references/mysql.md +137 -0
- package/skills/db-query/references/redis.md +213 -0
- package/skills/db-query/references/security.md +176 -0
- package/skills/db-query/scripts/lib/cli.js +118 -0
- package/skills/db-query/scripts/lib/config.js +215 -0
- package/skills/db-query/scripts/lib/mysql-readonly.js +655 -0
- package/skills/db-query/scripts/lib/output.js +322 -0
- package/skills/db-query/scripts/lib/redis-readonly.js +362 -0
- package/skills/db-query/scripts/mysql-query.js +198 -0
- package/skills/db-query/scripts/redis-query.js +181 -0
- package/skills/db-query/scripts/setup.sh +51 -0
- package/skills/git-commit-message/SKILL.md +49 -0
- package/skills/requirement-docs/SKILL.md +38 -0
- package/skills/requirement-docs/references/templates.md +187 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MySQL 只读查询入口
|
|
6
|
+
* 编排流程:CLI 解析 → SQL 只读校验 → 建连(只读事务+超时)→ 查询 → 输出 → 回滚关闭
|
|
7
|
+
* 安全要点:客户端 timer + 服务端 MAX_EXECUTION_TIME 双保险;永远 ROLLBACK
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// ── 内部模块 ──
|
|
14
|
+
const { parseCli, CliError } = require('./lib/cli');
|
|
15
|
+
const { loadEnvFile, resolveEnv, resolveEnvNumber, ConfigError, sanitizeTarget, safeMessage, buildTlsOptions } = require('./lib/config');
|
|
16
|
+
const { validateMysqlSql, ReadonlyError } = require('./lib/mysql-readonly');
|
|
17
|
+
const { formatMysqlTable, formatJson } = require('./lib/output');
|
|
18
|
+
|
|
19
|
+
// ── CLI 参数规格 ──
|
|
20
|
+
const MYSQL_SPEC = {
|
|
21
|
+
host: { type: 'string', default: '127.0.0.1' },
|
|
22
|
+
port: { type: 'integer', default: 3306, min: 1, max: 65535 },
|
|
23
|
+
user: { type: 'string', default: undefined },
|
|
24
|
+
password: { type: 'string', default: undefined },
|
|
25
|
+
database: { type: 'string', default: undefined },
|
|
26
|
+
tls: { type: 'boolean', default: false },
|
|
27
|
+
'ca-file': { type: 'string', default: undefined },
|
|
28
|
+
'connect-timeout-ms': { type: 'integer', default: 5000, min: 1000, max: 30000 },
|
|
29
|
+
'timeout-ms': { type: 'integer', default: 10000, min: 1000, max: 60000 },
|
|
30
|
+
'max-rows': { type: 'integer', default: 200, min: 1, max: 5000 },
|
|
31
|
+
'max-value-bytes': { type: 'integer', default: 4096, min: 256, max: 1048576 },
|
|
32
|
+
format: { type: 'string', default: 'table', choices: ['table', 'json'] },
|
|
33
|
+
'env-file': { type: 'string', default: undefined },
|
|
34
|
+
'validate-only': { type: 'boolean', default: false },
|
|
35
|
+
file: { type: 'string', default: undefined },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const ENV_PREFIX = 'DB_QUERY_MYSQL_';
|
|
39
|
+
|
|
40
|
+
/** 从文件读取 SQL(限 64KiB 普通文本) */
|
|
41
|
+
function readSqlFile(filePath) {
|
|
42
|
+
const resolved = path.resolve(filePath);
|
|
43
|
+
if (!fs.existsSync(resolved)) throw new Error(`SQL 文件不存在: ${resolved}`);
|
|
44
|
+
const stat = fs.statSync(resolved);
|
|
45
|
+
if (stat.size > 64 * 1024) throw new Error(`SQL 文件超过 64KiB: ${resolved}`);
|
|
46
|
+
return fs.readFileSync(resolved, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 打印校验结果(--validate-only 模式) */
|
|
50
|
+
function printValidation(checked) {
|
|
51
|
+
process.stderr.write(`✓ 校验通过: ${checked.kind}${checked.limitInjected ? ' (已注入 LIMIT 截断)' : ''}\n`);
|
|
52
|
+
process.stderr.write(`处理后的 SQL:\n${checked.sql}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 带超时的查询执行(客户端 timer + 服务端 MAX_EXECUTION_TIME 双保险) */
|
|
56
|
+
function queryWithTimeout(conn, sql, timeoutMs) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
// 客户端超时:销毁连接,避免服务端继续传输
|
|
60
|
+
conn.destroy();
|
|
61
|
+
reject(new Error(`查询超时 (${timeoutMs}ms)`));
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
|
|
64
|
+
conn.query(sql)
|
|
65
|
+
.then(result => { clearTimeout(timer); resolve(result); })
|
|
66
|
+
.catch(err => { clearTimeout(timer); reject(err); });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 安全回滚(忽略错误,确保连接一定关闭) */
|
|
71
|
+
async function safeRollback(conn) {
|
|
72
|
+
try { await conn.query('ROLLBACK'); } catch (_) { /* 事务不存在等错误忽略 */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 安全关闭连接 */
|
|
76
|
+
async function safeClose(conn) {
|
|
77
|
+
try { await conn.end(); } catch (_) { /* 连接已断开等错误忽略 */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 主函数 ──
|
|
81
|
+
async function main() {
|
|
82
|
+
const { options, rest } = parseCli(process.argv.slice(2), MYSQL_SPEC);
|
|
83
|
+
|
|
84
|
+
// SQL 来源校验:--file 和位置参数二选一
|
|
85
|
+
if (options.file && rest.length > 0) {
|
|
86
|
+
throw new CliError('--file 和位置参数 SQL 不能同时指定');
|
|
87
|
+
}
|
|
88
|
+
if (!options.file && rest.length === 0) {
|
|
89
|
+
throw new CliError('必须指定 SQL(位置参数或 --file)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 读取 SQL(在任何连接操作之前)
|
|
93
|
+
const rawSql = options.file ? readSqlFile(options.file) : rest[0];
|
|
94
|
+
|
|
95
|
+
// 只读校验(纯离线,不依赖任何连接参数)
|
|
96
|
+
const maxRows = options.maxRows;
|
|
97
|
+
const checked = validateMysqlSql(rawSql, { maxRows });
|
|
98
|
+
|
|
99
|
+
// --validate-only:纯离线校验,不解析凭据、不建连
|
|
100
|
+
if (options['validate-only']) {
|
|
101
|
+
return printValidation(checked);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 以下为连库路径,需要连接参数 ──
|
|
105
|
+
// 修复:env 文件加载必须在 user/database 必填检查之前,否则 env 文件提供的凭据读不到
|
|
106
|
+
const envPairs = loadEnvFile(options['env-file']);
|
|
107
|
+
|
|
108
|
+
// 合并环境变量覆盖(统一用 resolveEnvNumber 做边界校验,防 env 文件绕过硬上限)
|
|
109
|
+
const host = resolveEnv(envPairs, ENV_PREFIX + 'HOST', options.host);
|
|
110
|
+
const port = resolveEnvNumber(envPairs, ENV_PREFIX + 'PORT', options.port, { min: 1, max: 65535 });
|
|
111
|
+
const user = resolveEnv(envPairs, ENV_PREFIX + 'USER', options.user);
|
|
112
|
+
const password = resolveEnv(envPairs, ENV_PREFIX + 'PASSWORD', options.password);
|
|
113
|
+
const database = resolveEnv(envPairs, ENV_PREFIX + 'DATABASE', options.database);
|
|
114
|
+
|
|
115
|
+
// user 必填;database 放宽为可选——排查第一步常是 SHOW DATABASES / SELECT 1 /
|
|
116
|
+
// 带库名前缀的表查询,不强制默认库;查表未指定库时 MySQL 会自报 ER_NO_DB_ERROR
|
|
117
|
+
if (!user) throw new CliError('缺少 user(用 --user、DB_QUERY_MYSQL_USER、环境变量或 env 文件提供)');
|
|
118
|
+
if (!database) {
|
|
119
|
+
process.stderr.write('(未指定 database:仅能执行无默认库的查询,如 SHOW DATABASES / SELECT 1 / 带库名前缀的表)\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const maxRowsResolved = resolveEnvNumber(envPairs, ENV_PREFIX + 'MAX_ROWS', options.maxRows, { min: 1, max: 5000 });
|
|
123
|
+
const maxValueBytes = resolveEnvNumber(envPairs, 'DB_QUERY_MAX_VALUE_BYTES', options['max-value-bytes'], { min: 256, max: 1048576 });
|
|
124
|
+
const timeoutMs = resolveEnvNumber(envPairs, ENV_PREFIX + 'TIMEOUT_MS', options['timeout-ms'], { min: 1000, max: 60000 });
|
|
125
|
+
const connectTimeoutMs = resolveEnvNumber(envPairs, ENV_PREFIX + 'CONNECT_TIMEOUT_MS', options['connect-timeout-ms'], { min: 1000, max: 30000 });
|
|
126
|
+
|
|
127
|
+
// 打印脱敏目标(连库前确认去向,绝不暴露密码)
|
|
128
|
+
const target = sanitizeTarget({ host, port, user, database });
|
|
129
|
+
process.stderr.write(`→ MySQL ${target}\n`);
|
|
130
|
+
|
|
131
|
+
// 动态加载 mysql2(只在需要连库时)
|
|
132
|
+
const mysql = require('mysql2/promise');
|
|
133
|
+
|
|
134
|
+
const conn = await mysql.createConnection({
|
|
135
|
+
host,
|
|
136
|
+
port,
|
|
137
|
+
user,
|
|
138
|
+
password: password || undefined,
|
|
139
|
+
database,
|
|
140
|
+
connectTimeout: connectTimeoutMs,
|
|
141
|
+
multipleStatements: false, // 显式关闭,不依赖驱动默认值
|
|
142
|
+
supportBigNumbers: true,
|
|
143
|
+
bigNumberStrings: true, // BIGINT/DECIMAL 保留字符串精度
|
|
144
|
+
decimalNumbers: false,
|
|
145
|
+
dateStrings: true, // 日期保留服务端文本
|
|
146
|
+
...(options.tls || options['ca-file']
|
|
147
|
+
? { ssl: buildTlsOptions(options['ca-file']) || { rejectUnauthorized: true } }
|
|
148
|
+
: {}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const startMs = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
// 服务端只读事务(不支持的版本会报错,fail-closed 不静默降级)
|
|
154
|
+
await conn.query('SET SESSION TRANSACTION READ ONLY');
|
|
155
|
+
await conn.query('SET SESSION MAX_EXECUTION_TIME = ?', [timeoutMs]);
|
|
156
|
+
await conn.query('START TRANSACTION READ ONLY');
|
|
157
|
+
|
|
158
|
+
const [rows, fields] = await queryWithTimeout(conn, checked.sql, timeoutMs);
|
|
159
|
+
const elapsedMs = Date.now() - startMs;
|
|
160
|
+
|
|
161
|
+
// 判断是否有截断(LIMIT 注入了 maxRows+1 哨兵行)
|
|
162
|
+
const truncated = checked.limitInjected && rows.length > maxRowsResolved;
|
|
163
|
+
const displayRows = truncated ? rows.slice(0, maxRowsResolved) : rows;
|
|
164
|
+
|
|
165
|
+
if (options.format === 'json') {
|
|
166
|
+
// JSON 写 stdout,状态写 stderr,保证 stdout 是合法 JSON
|
|
167
|
+
process.stdout.write(JSON.stringify({
|
|
168
|
+
source: 'mysql',
|
|
169
|
+
target,
|
|
170
|
+
elapsedMs,
|
|
171
|
+
count: truncated ? maxRowsResolved : rows.length,
|
|
172
|
+
truncated,
|
|
173
|
+
result: displayRows,
|
|
174
|
+
}, null, 2) + '\n');
|
|
175
|
+
} else {
|
|
176
|
+
process.stdout.write(formatMysqlTable(displayRows, fields, {
|
|
177
|
+
maxRows: maxRowsResolved,
|
|
178
|
+
maxValueBytes,
|
|
179
|
+
truncated,
|
|
180
|
+
}) + '\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.stderr.write(`✓ ${rows.length} 行${truncated ? '(已截断)' : ''},${elapsedMs}ms\n`);
|
|
184
|
+
} finally {
|
|
185
|
+
// 永远 ROLLBACK,不发 COMMIT(防御性,确保不留事务)
|
|
186
|
+
await safeRollback(conn);
|
|
187
|
+
await safeClose(conn);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch(e => {
|
|
192
|
+
if (e instanceof CliError || e instanceof ReadonlyError || e instanceof ConfigError) {
|
|
193
|
+
process.stderr.write('✗ ' + e.message + '\n');
|
|
194
|
+
} else {
|
|
195
|
+
process.stderr.write('✗ ' + safeMessage(e) + '\n');
|
|
196
|
+
}
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis 只读查询入口
|
|
6
|
+
* 编排流程:CLI 解析 → 命令白名单校验 → 建连 → sendCommand → 输出 → 关闭
|
|
7
|
+
* 安全要点:db 只通过连接配置选择(不开放 SELECT 命令);
|
|
8
|
+
* 客户端 timer 超时 destroy socket;
|
|
9
|
+
* Redis ACL 只读用户是服务端最终边界
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { createClient } = require('redis');
|
|
13
|
+
|
|
14
|
+
// ── 内部模块 ──
|
|
15
|
+
const { parseCli, CliError } = require('./lib/cli');
|
|
16
|
+
const { loadEnvFile, resolveEnv, resolveEnvNumber, ConfigError, sanitizeTarget, safeMessage, buildTlsOptions } = require('./lib/config');
|
|
17
|
+
const { validateRedisCommand, RedisReadonlyError } = require('./lib/redis-readonly');
|
|
18
|
+
const { formatRedisTable, formatJson } = require('./lib/output');
|
|
19
|
+
|
|
20
|
+
// ── CLI 参数规格 ──
|
|
21
|
+
const REDIS_SPEC = {
|
|
22
|
+
host: { type: 'string', default: '127.0.0.1' },
|
|
23
|
+
port: { type: 'integer', default: 6379, min: 1, max: 65535 },
|
|
24
|
+
user: { type: 'string', default: 'default' },
|
|
25
|
+
password: { type: 'string', default: undefined },
|
|
26
|
+
db: { type: 'integer', default: 0, min: 0, max: 15 },
|
|
27
|
+
tls: { type: 'boolean', default: false },
|
|
28
|
+
'ca-file': { type: 'string', default: undefined },
|
|
29
|
+
'connect-timeout-ms': { type: 'integer', default: 5000, min: 1000, max: 30000 },
|
|
30
|
+
'timeout-ms': { type: 'integer', default: 10000, min: 1000, max: 60000 },
|
|
31
|
+
'max-items': { type: 'integer', default: 200, min: 1, max: 5000 },
|
|
32
|
+
'max-value-bytes': { type: 'integer', default: 4096, min: 256, max: 1048576 },
|
|
33
|
+
format: { type: 'string', default: 'table', choices: ['table', 'json'] },
|
|
34
|
+
'env-file': { type: 'string', default: undefined },
|
|
35
|
+
'validate-only': { type: 'boolean', default: false },
|
|
36
|
+
'allow-unbounded': { type: 'boolean', default: false },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ENV_PREFIX = 'DB_QUERY_REDIS_';
|
|
40
|
+
|
|
41
|
+
// ── 主函数 ──
|
|
42
|
+
async function main() {
|
|
43
|
+
const { options, rest } = parseCli(process.argv.slice(2), REDIS_SPEC);
|
|
44
|
+
|
|
45
|
+
// -- 后面必须是 Redis 命令
|
|
46
|
+
if (rest.length === 0) {
|
|
47
|
+
throw new CliError('必须在 -- 之后指定 Redis 命令(如 -- GET key)');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 命令白名单校验(在任何连接操作之前)
|
|
51
|
+
const validated = validateRedisCommand(rest, {
|
|
52
|
+
maxItems: options.maxItems,
|
|
53
|
+
allowUnbounded: options['allow-unbounded'],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// --validate-only:纯离线校验,不建连
|
|
57
|
+
if (options['validate-only']) {
|
|
58
|
+
process.stderr.write(`✓ 命令校验通过: ${validated.command} ${validated.args.join(' ')}\n`);
|
|
59
|
+
process.stderr.write(`输出形状: ${validated.shape}${validated.unbounded ? ' (无界)' : ''}\n`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// env 文件加载
|
|
64
|
+
const envPairs = loadEnvFile(options['env-file']);
|
|
65
|
+
|
|
66
|
+
// 合并环境变量覆盖(统一用 resolveEnvNumber 做边界校验,防 env 文件绕过硬上限)
|
|
67
|
+
const host = resolveEnv(envPairs, ENV_PREFIX + 'HOST', options.host);
|
|
68
|
+
const port = resolveEnvNumber(envPairs, ENV_PREFIX + 'PORT', options.port, { min: 1, max: 65535 });
|
|
69
|
+
const user = resolveEnv(envPairs, ENV_PREFIX + 'USER', options.user);
|
|
70
|
+
const password = resolveEnv(envPairs, ENV_PREFIX + 'PASSWORD', options.password);
|
|
71
|
+
const db = resolveEnvNumber(envPairs, ENV_PREFIX + 'DB', options.db, { min: 0, max: 15 });
|
|
72
|
+
const maxItems = resolveEnvNumber(envPairs, ENV_PREFIX + 'MAX_ITEMS', options.maxItems, { min: 1, max: 5000 });
|
|
73
|
+
const maxItemsResolved = maxItems;
|
|
74
|
+
const maxValueBytes = resolveEnvNumber(envPairs, 'DB_QUERY_MAX_VALUE_BYTES', options['max-value-bytes'], { min: 256, max: 1048576 });
|
|
75
|
+
const maxValueBytesResolved = maxValueBytes;
|
|
76
|
+
const timeoutMs = resolveEnvNumber(envPairs, ENV_PREFIX + 'TIMEOUT_MS', options['timeout-ms'], { min: 1000, max: 60000 });
|
|
77
|
+
const timeoutMsResolved = timeoutMs;
|
|
78
|
+
const connectTimeoutMs = resolveEnvNumber(envPairs, ENV_PREFIX + 'CONNECT_TIMEOUT_MS', options['connect-timeout-ms'], { min: 1000, max: 30000 });
|
|
79
|
+
const connectTimeoutMsResolved = connectTimeoutMs;
|
|
80
|
+
|
|
81
|
+
// 打印脱敏目标
|
|
82
|
+
const target = sanitizeTarget({ host, port, user, database: db });
|
|
83
|
+
process.stderr.write(`→ Redis ${target}\n`);
|
|
84
|
+
|
|
85
|
+
// 构建 node-redis v4 客户端
|
|
86
|
+
const clientConfig = {
|
|
87
|
+
socket: {
|
|
88
|
+
host,
|
|
89
|
+
port,
|
|
90
|
+
connectTimeout: connectTimeoutMs,
|
|
91
|
+
// TLS
|
|
92
|
+
...(options.tls || options['ca-file']
|
|
93
|
+
? { tls: buildTlsOptions(options['ca-file']) || { rejectUnauthorized: true } }
|
|
94
|
+
: {}),
|
|
95
|
+
},
|
|
96
|
+
database: db,
|
|
97
|
+
password: password || undefined,
|
|
98
|
+
username: user,
|
|
99
|
+
// 保留 Buffer 返回,避免自动字符串化丢二进制
|
|
100
|
+
returnBuffers: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const client = createClient(clientConfig);
|
|
104
|
+
|
|
105
|
+
// 超时定时器(connect + sendCommand 总超时)
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
client.destroy();
|
|
108
|
+
// 不直接 reject(Promise 可能已 settle),通过 error 事件处理
|
|
109
|
+
}, timeoutMsResolved);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await client.connect();
|
|
113
|
+
|
|
114
|
+
// sendCommand 以数组形式传参,绝不拼接后 split(防止参数含空格被截断)
|
|
115
|
+
const cmdArgs = [validated.command, ...validated.args.map(String)];
|
|
116
|
+
const startMs = Date.now();
|
|
117
|
+
const result = await client.sendCommand(cmdArgs);
|
|
118
|
+
const elapsedMs = Date.now() - startMs;
|
|
119
|
+
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
|
|
122
|
+
// 判断截断
|
|
123
|
+
let count = 0;
|
|
124
|
+
let truncated = false;
|
|
125
|
+
if (Array.isArray(result)) {
|
|
126
|
+
count = result.length;
|
|
127
|
+
truncated = count > maxItemsResolved;
|
|
128
|
+
} else {
|
|
129
|
+
count = 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.format === 'json') {
|
|
133
|
+
// JSON envelope 写 stdout
|
|
134
|
+
process.stdout.write(JSON.stringify({
|
|
135
|
+
source: 'redis',
|
|
136
|
+
target,
|
|
137
|
+
elapsedMs,
|
|
138
|
+
count: truncated ? maxItemsResolved : count,
|
|
139
|
+
truncated,
|
|
140
|
+
// Buffer → base64 以确保 JSON 安全
|
|
141
|
+
result: sanitizeForJson(result),
|
|
142
|
+
}, null, 2) + '\n');
|
|
143
|
+
} else {
|
|
144
|
+
process.stdout.write(formatRedisTable(result, {
|
|
145
|
+
shape: validated.shape,
|
|
146
|
+
command: validated.command,
|
|
147
|
+
maxItems: maxItemsResolved,
|
|
148
|
+
maxValueBytes: maxValueBytesResolved,
|
|
149
|
+
}) + '\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
process.stderr.write(`✓ ${count} 条${truncated ? '(已截断)' : ''},${elapsedMs}ms\n`);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
throw e;
|
|
156
|
+
} finally {
|
|
157
|
+
try { await client.quit(); } catch (_) { /* 忽略关闭错误 */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 递归将 Buffer 转为 base64,确保 JSON 安全 */
|
|
162
|
+
function sanitizeForJson(val) {
|
|
163
|
+
if (Buffer.isBuffer(val)) return val.toString('base64');
|
|
164
|
+
if (Array.isArray(val)) return val.map(sanitizeForJson);
|
|
165
|
+
if (val && typeof val === 'object' && val.constructor === Object) {
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const k of Object.keys(val)) out[k] = sanitizeForJson(val[k]);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
return val;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
main().catch(e => {
|
|
174
|
+
// 校验类/配置类错误直接显示消息(不是连接错误,不需要脱敏)
|
|
175
|
+
if (e instanceof CliError || e instanceof RedisReadonlyError || e instanceof ConfigError) {
|
|
176
|
+
process.stderr.write('✗ ' + e.message + '\n');
|
|
177
|
+
} else {
|
|
178
|
+
process.stderr.write('✗ ' + safeMessage(e) + '\n');
|
|
179
|
+
}
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# db-query 技能一键环境检查 + 安装 + 测试
|
|
3
|
+
# 幂等:依赖已装则跳过 npm ci
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
cd "$SCRIPT_DIR"
|
|
9
|
+
|
|
10
|
+
# ── Node / npm 版本检查 ──
|
|
11
|
+
if ! command -v node &>/dev/null; then
|
|
12
|
+
echo "✗ 未找到 node,需要 Node.js >= 18"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
|
|
17
|
+
if [ "$NODE_MAJOR" -lt 18 ]; then
|
|
18
|
+
echo "✗ Node.js 版本 $(node -v) 低于最低要求 18"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if ! command -v npm &>/dev/null; then
|
|
23
|
+
echo "✗ 未找到 npm"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "Node $(node -v) ✓ npm $(npm -v) ✓"
|
|
28
|
+
|
|
29
|
+
# ── 幂等安装运行时依赖 ──
|
|
30
|
+
# 已有 node_modules 时 npm ci 仍会检查一致性,幂等安全
|
|
31
|
+
if [ -d node_modules ]; then
|
|
32
|
+
echo "依赖已存在,跳过安装(删除 node_modules 可强制重装)"
|
|
33
|
+
else
|
|
34
|
+
echo "安装运行时依赖(mysql2 redis dotenv)..."
|
|
35
|
+
if [ -f package-lock.json ]; then
|
|
36
|
+
npm ci --omit=dev --no-audit --no-fund
|
|
37
|
+
else
|
|
38
|
+
npm install --omit=dev --no-audit --no-fund
|
|
39
|
+
fi
|
|
40
|
+
echo "依赖安装 ✓"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ── 运行测试 ──
|
|
44
|
+
echo "运行测试..."
|
|
45
|
+
if node --test tests/*.test.js 2>/dev/null; then
|
|
46
|
+
echo "全部测试通过 ✓"
|
|
47
|
+
else
|
|
48
|
+
echo "⚠ 未找到测试文件或测试未通过(首次部署无测试属正常)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "db-query 环境就绪 ✓"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit-message
|
|
3
|
+
description: Generate convention-compliant Git commit messages. Trigger whenever the intent is to produce commit text (commit message/description), including "generate the commit description/message" after a batch of changes — even without the word "commit". Text only; never commits or pushes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- [2026-09-05]-[default output to English unless project rules/user explicitly request Chinese; generic examples]-[generated messages follow this default] -->
|
|
7
|
+
<!-- [2026-10-01]-[Language section: honor a [SWITCHMAN:LANG] system-prompt line over the English default]-[aligns with the host-side language-preference layer] -->
|
|
8
|
+
# Git Commit Message Generation
|
|
9
|
+
|
|
10
|
+
Core rule: **deliver text only; never run `git add` / `git commit` / `git push`**.
|
|
11
|
+
|
|
12
|
+
Language: **English by default**. Use the project's configured language when the project's rules (AGENTS.md etc.) explicitly specify one, or when the user explicitly requests it.
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
### 1. Confirm the task ID (always first)
|
|
17
|
+
|
|
18
|
+
- Not provided → ask once ("task ID for this commit?"); never invent it
|
|
19
|
+
- User explicitly declines → omit the ID (only exception); don't re-ask within the same session
|
|
20
|
+
|
|
21
|
+
### 2. Analyze changes
|
|
22
|
+
|
|
23
|
+
Run as needed, not mechanically: `git status` (files changed), `git diff` / `git diff --cached` (content), `git log --oneline -5` (repo style). Reuse this conversation's context when available to run fewer commands.
|
|
24
|
+
|
|
25
|
+
### 3. Generate the message
|
|
26
|
+
|
|
27
|
+
**Subject** (concise, one line): `<type>(<task-id>): <what was done>`; without ID `<type>: <what was done>`. State the result, not the process; no trailing period.
|
|
28
|
+
|
|
29
|
+
type: `feat` / `fix` / `refactor` / `docs` / `test` / `perf` / `style` / `chore`.
|
|
30
|
+
|
|
31
|
+
**Body**: numbered points, usually 2~4 (1 if trivial); one key change per point, no diff restatement.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
refactor(1024): Extract config parsing into loader module
|
|
37
|
+
|
|
38
|
+
1. Move inline parsing logic into config/loader.ts
|
|
39
|
+
2. Add validation for missing required fields
|
|
40
|
+
3. Extend tests to cover the new module
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 4. Stop after delivery
|
|
44
|
+
|
|
45
|
+
Present the message and stop; don't ask "want me to commit?". Commit/push only on explicit request.
|
|
46
|
+
|
|
47
|
+
## Language
|
|
48
|
+
|
|
49
|
+
English by default; use the project's configured language when a `[SWITCHMAN:LANG]` line in the system prompt specifies one, or when the user explicitly requests it.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: requirement-docs
|
|
3
|
+
description: Unified spec for technical docs — requirements analysis, PRD, design, architecture, API design, tech plan, dev plan. Trigger before drafting or reorganizing any such doc; archive to docs/requirements-and-design/ with unified naming. English by default.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- [2026-09-05]-[default output to English unless project rules/user explicitly request Chinese; generic examples]-[generated docs follow this default] -->
|
|
7
|
+
<!-- [2026-10-01]-[Language section: honor a [SWITCHMAN:LANG] system-prompt line over the English default]-[aligns with the host-side language-preference layer] -->
|
|
8
|
+
# Requirements & Design Doc Spec
|
|
9
|
+
|
|
10
|
+
Goal: centralized archiving, unified naming, consistent structure; understand existing code before proposing solutions.
|
|
11
|
+
|
|
12
|
+
Language: **English by default** (keep technical terms as-is). Use the project's configured language when the project's rules (AGENTS.md etc.) explicitly specify one, or when the user explicitly requests it.
|
|
13
|
+
|
|
14
|
+
## 1. Flow (every time)
|
|
15
|
+
|
|
16
|
+
1. Locate repo root, `mkdir -p docs/requirements-and-design/` (reuse the repo's existing archive dir if present); scan existing files to avoid name clashes and style drift.
|
|
17
|
+
2. Name as `yyyy-mm-dd-<module>-<type>.md`: today's date; module in kebab-case; type word in the doc's language — `requirements` / `PRD` / `design` / `architecture` / `api-design` / `tech-plan` / `dev-plan` (Chinese docs: 需求分析/PRD/设计/架构设计/接口设计/技术方案/开发计划).
|
|
18
|
+
E.g. `docs/requirements-and-design/2026-09-05-config-loader-design.md`
|
|
19
|
+
3. Research first: read related code, `docs/`, `README`, `AGENTS.md`, key entities/services before writing.
|
|
20
|
+
4. Read `references/templates.md` (mandatory) to pick a skeleton; it defines structure only — render section titles and content in the output language. Self-check after writing (date, type word, section style, [Decided]/[Pending] markers, appendix), then report the file path to the user.
|
|
21
|
+
|
|
22
|
+
## 2. Core principles (legacy-system methodology)
|
|
23
|
+
|
|
24
|
+
1. **Evidence-backed current-state analysis**: trace call chains from entry points (Controller/API/CLI) to core logic with `file:line`; list reusable assets and reference implementations; call out existing pitfalls (swallowed exceptions, hardcoding, etc.). No empty "the system currently has X" statements.
|
|
25
|
+
2. **Current state → gaps → solution**: current-state analysis first, then a gap table (numbered D1/D2…, current vs required); the solution references gap IDs point by point.
|
|
26
|
+
3. **Honest [Decided]/[Pending] split** (Chinese: 【已定】/【待定】): confirmed decisions in a quote block at the top with confirmation dates; open questions in their own section, each with a recommended lean; every risk carries a mitigation. Never present pending as decided.
|
|
27
|
+
4. **Field-level clarity & appendix**: for key fields state exactly which entity/field they come from; expand ambiguous spots with a comparison and a recommendation; end with a file:line quick-reference table.
|
|
28
|
+
|
|
29
|
+
## 3. Writing rules
|
|
30
|
+
|
|
31
|
+
1. Wrap code, paths, field names in backticks.
|
|
32
|
+
2. Prefer mermaid or indented code blocks for call chains/sequences/flows; no external images.
|
|
33
|
+
3. Give reasons for decisions, not just conclusions.
|
|
34
|
+
4. When citing `file:line`, include the method name/context to survive line drift.
|
|
35
|
+
|
|
36
|
+
## Language
|
|
37
|
+
|
|
38
|
+
English by default; use the project's configured language when a `[SWITCHMAN:LANG]` line in the system prompt specifies one, or when the user explicitly requests it.
|