@optima-chat/dev-skills 0.13.0 → 0.13.1
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/.claude/commands/query-db.md +13 -11
- package/.claude/skills/query-db/SKILL.md +36 -6
- package/.codex/skills/query-db/SKILL.md +12 -1
- package/AGENTS.md +4 -4
- package/bin/helpers/query-db.ts +115 -15
- package/dist/bin/helpers/query-db.js +103 -14
- package/package.json +5 -3
|
@@ -200,13 +200,14 @@ print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
|
200
200
|
" > /tmp/stage_db_config.sh && source /tmp/stage_db_config.sh
|
|
201
201
|
|
|
202
202
|
# 4. 建立 SSH 隧道到 Shared EC2,通过隧道访问 Stage RDS
|
|
203
|
-
|
|
203
|
+
# ⚠️ 先确认本地端口空闲(ss -ltn | grep 25432)——端口被本机 Docker PG 占用时,凭证会发给错误的库、报「密码错误」
|
|
204
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 25432:optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com:5432 ec2-user@3.0.210.113
|
|
204
205
|
|
|
205
|
-
# 5. 通过本地端口
|
|
206
|
-
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p
|
|
206
|
+
# 5. 通过本地端口 25432 连接到 RDS
|
|
207
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 25432 -U "${COMMERCE_DB_USER}" -d optima_commerce -c "SELECT COUNT(*) FROM products"
|
|
207
208
|
|
|
208
209
|
# 6. 关闭 SSH 隧道(可选)
|
|
209
|
-
pkill -f "ssh.*
|
|
210
|
+
pkill -f "ssh.*25432:${DATABASE_HOST}:5432"
|
|
210
211
|
```
|
|
211
212
|
|
|
212
213
|
**完整示例(五个服务)**:
|
|
@@ -283,7 +284,7 @@ pkill -f "ssh.*15432:${DATABASE_HOST}:5432"
|
|
|
283
284
|
- billing、ads-backend、amazon-backend、browser-backend、shopify-backend、optima-generation、optima-sentinel 的凭证存在各自服务路径的 `DATABASE_URL` 中
|
|
284
285
|
- Stage RDS: `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
285
286
|
- Shared EC2 IP: `3.0.210.113`
|
|
286
|
-
- SSH 隧道: 本地端口 `
|
|
287
|
+
- SSH 隧道: 本地端口 `25432` → Shared EC2 → Stage RDS `5432`
|
|
287
288
|
- Stage 和 Prod 有独立的 RDS 实例
|
|
288
289
|
- ⚠️ session-gateway 数据库名: Stage 用 `optima_shell`, Prod 用 `optima_ai_shell`
|
|
289
290
|
|
|
@@ -323,13 +324,14 @@ print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
|
323
324
|
" > /tmp/prod_db_config.sh && source /tmp/prod_db_config.sh
|
|
324
325
|
|
|
325
326
|
# 4. 建立 SSH 隧道到 Shared EC2,通过隧道访问 Prod RDS
|
|
326
|
-
|
|
327
|
+
# ⚠️ 先确认本地端口空闲(ss -ltn | grep 25433)——端口被本机 Docker PG 占用时,凭证会发给错误的库、报「密码错误」
|
|
328
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 25433:optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com:5432 ec2-user@3.0.210.113
|
|
327
329
|
|
|
328
|
-
# 5. 通过本地端口
|
|
329
|
-
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p
|
|
330
|
+
# 5. 通过本地端口 25433 连接到 RDS
|
|
331
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 25433 -U "${COMMERCE_DB_USER}" -d optima_commerce -c "SELECT COUNT(*) FROM products"
|
|
330
332
|
|
|
331
333
|
# 6. 关闭 SSH 隧道(可选)
|
|
332
|
-
pkill -f "ssh.*
|
|
334
|
+
pkill -f "ssh.*25433:${DATABASE_HOST}:5432"
|
|
333
335
|
```
|
|
334
336
|
|
|
335
337
|
**完整示例(五个服务)**:
|
|
@@ -394,8 +396,8 @@ pkill -f "ssh.*15433:${DATABASE_HOST}:5432"
|
|
|
394
396
|
- billing、browser-backend、optima-generation 的凭证存在各自服务路径的 DATABASE_URL 中
|
|
395
397
|
- Prod RDS: `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
396
398
|
- Shared EC2 IP: `3.0.210.113`
|
|
397
|
-
- SSH 隧道: 本地端口 `
|
|
398
|
-
- Stage 用端口 `
|
|
399
|
+
- SSH 隧道: 本地端口 `25433` → Shared EC2 → Prod RDS `5432`
|
|
400
|
+
- Stage 用端口 `25432`,Prod 用端口 `25433`
|
|
399
401
|
- Stage 和 Prod 有独立的 RDS 实例
|
|
400
402
|
- ⚠️ session-gateway 数据库名: Stage 用 `optima_shell`, Prod 用 `optima_ai_shell`
|
|
401
403
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "query-db"
|
|
3
|
-
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod
|
|
3
|
+
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod、cn-prod、cn-stage(阿里云)五个环境的 commerce-backend、user-auth、agentic-chat、bi-backend、session-gateway、gateway-core、ads-backend、amazon-backend、shopify-backend、optima-sentinel 等服务的数据库查询。优先使用 optima-query-db CLI 工具。"
|
|
4
4
|
allowed-tools: ["Bash", "SlashCommand"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,6 +16,8 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
16
16
|
optima-query-db <service> "<sql>" [environment]
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
**环境怎么传**:environment 取 `ci`(默认)/ `stage` / `prod` / `cn-prod` / `cn-stage`,作为**第 3 个位置参数**(`optima-query-db gateway-core "SELECT 1" cn-stage`)或 **`--env` 旗标**(`--env cn-stage`,短形式 `-e`、`--env=cn-stage` 也认,与 optima-logs 一致)。除 `--env`/`-e` 外没有其他旗标;未知旗标、多余参数、未知环境名、纯注释 SQL 都会硬报错并打印 usage,绝不静默空输出(背景见 issue #60)。
|
|
20
|
+
|
|
19
21
|
**为什么使用 CLI 工具**:
|
|
20
22
|
- ✅ 统一实现,避免重复代码
|
|
21
23
|
- ✅ 自动处理所有环境差异
|
|
@@ -60,6 +62,12 @@ optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
|
|
|
60
62
|
|
|
61
63
|
# Prod 环境
|
|
62
64
|
optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY status" prod
|
|
65
|
+
|
|
66
|
+
# cn-stage 环境(阿里云预发;也可写成 --env cn-stage)
|
|
67
|
+
optima-query-db gateway-core "SELECT COUNT(*) FROM conversations" cn-stage
|
|
68
|
+
|
|
69
|
+
# cn-prod 环境(阿里云生产)
|
|
70
|
+
optima-query-db billing "SELECT COUNT(*) FROM credit_ledger" cn-prod
|
|
63
71
|
```
|
|
64
72
|
|
|
65
73
|
### 使用 Slash 命令(备用)
|
|
@@ -184,6 +192,26 @@ optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY s
|
|
|
184
192
|
- 通过 SSH 隧道访问 RDS
|
|
185
193
|
- ⚠️ 谨慎使用
|
|
186
194
|
|
|
195
|
+
### cn-stage / cn-prod 环境(阿里云)
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
optima-query-db gateway-core "SELECT COUNT(*) FROM conversations" cn-stage
|
|
199
|
+
optima-query-db gateway-core "SELECT key, value FROM app_configs" cn-prod
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**特点**:
|
|
203
|
+
- 阿里云侧独立部署:cn-stage(预发)与 cn-prod(生产)是**两个独立 RDS 实例**(共用 prod VPC 与 buildbox 跳板)
|
|
204
|
+
- 独立 Infisical 实例(secrets-cn.optima.chat,单实例双环境:cn-prod→`prod`、cn-stage→`staging`),与 AWS 侧 Infisical 完全无关
|
|
205
|
+
- 经 buildbox ECS 跳板自动建 SSH 隧道连内网 RDS
|
|
206
|
+
- ⚠️ cn-prod 是生产真实用户数据,谨慎使用
|
|
207
|
+
|
|
208
|
+
**前置环境变量**(缺失时命令会报错并提示,详见 https://github.com/Optima-Chat/optima-dev-skills/issues/21 ):
|
|
209
|
+
|
|
210
|
+
| 变量 | 用途 | 来源 |
|
|
211
|
+
|------|------|------|
|
|
212
|
+
| `INFISICAL_CN_EMAIL` + `INFISICAL_CN_PASSWORD` | 登录 cn Infisical 取数据库凭证 | 1Password「Infisical cn-prod admin (secrets-cn.optima.chat)」 |
|
|
213
|
+
| `OPTIMA_CN_BUILDBOX_PASSWORD` | 经 buildbox 跳板建 RDS 隧道(本机已有健康隧道时可省) | 1Password「Aliyun cn-prod buildbox ECS (root)」 |
|
|
214
|
+
|
|
187
215
|
## 🔧 技术架构
|
|
188
216
|
|
|
189
217
|
### Infisical 配置(v0.7.0+)
|
|
@@ -217,12 +245,14 @@ optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY s
|
|
|
217
245
|
|
|
218
246
|
### RDS 连接
|
|
219
247
|
|
|
220
|
-
| 环境 | RDS Host |
|
|
221
|
-
|
|
222
|
-
| Stage | `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` |
|
|
223
|
-
| Prod | `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` |
|
|
248
|
+
| 环境 | RDS Host | 跳板 |
|
|
249
|
+
|------|----------|------|
|
|
250
|
+
| Stage | `optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` | Shared EC2 `3.0.210.113` |
|
|
251
|
+
| Prod | `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com` | Shared EC2 `3.0.210.113` |
|
|
252
|
+
| cn-stage | `pgm-2zem1u9zdh06boim.pg.rds.aliyuncs.com` | buildbox ECS `47.94.105.163` |
|
|
253
|
+
| cn-prod | `pgm-2zexwx9eso9e4yla.pg.rds.aliyuncs.com` | buildbox ECS `47.94.105.163` |
|
|
224
254
|
|
|
225
|
-
|
|
255
|
+
**本地端口**:动态分配(自 25432 起扫描空闲端口,记录在 `~/.cache/optima-dev-skills/tunnel-ports.json`,复用前校验隧道健康);旧文档的固定 15432/15433 已废弃——固定端口会撞本机 Docker PG。
|
|
226
256
|
|
|
227
257
|
## 🔗 相关命令
|
|
228
258
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "query-db"
|
|
3
|
-
description: "Use when the user asks to query Optima databases, inspect rows, validate data, or run SQL in CI, Stage, or
|
|
3
|
+
description: "Use when the user asks to query Optima databases, inspect rows, validate data, or run SQL in CI, Stage, Prod, cn-prod, or cn-stage."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Query Databases
|
|
@@ -15,6 +15,17 @@ Always prefer:
|
|
|
15
15
|
optima-query-db <service> "<sql>" [environment]
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Environments
|
|
19
|
+
|
|
20
|
+
`ci` (default), `stage`, `prod`, `cn-prod` (Aliyun production), `cn-stage` (Aliyun staging).
|
|
21
|
+
|
|
22
|
+
Pass the environment as the 3rd positional argument (`optima-query-db gateway-core "SELECT 1" cn-stage`) or via the `--env` flag (`--env cn-stage`, consistent with optima-logs). Any other flag, extra positional argument, unknown environment name, or comment-only SQL is rejected with a hard error instead of a silent empty result (see issue #60).
|
|
23
|
+
|
|
24
|
+
For `cn-prod` / `cn-stage`, two extra env vars are required (the CLI prints an actionable error when missing; see https://github.com/Optima-Chat/optima-dev-skills/issues/21 ):
|
|
25
|
+
|
|
26
|
+
- `INFISICAL_CN_EMAIL` + `INFISICAL_CN_PASSWORD` — cn Infisical login (1Password "Infisical cn-prod admin (secrets-cn.optima.chat)")
|
|
27
|
+
- `OPTIMA_CN_BUILDBOX_PASSWORD` — buildbox ECS root password for the RDS tunnel (1Password "Aliyun cn-prod buildbox ECS (root)"); optional when a healthy tunnel already exists
|
|
28
|
+
|
|
18
29
|
## Services
|
|
19
30
|
|
|
20
31
|
- `commerce-backend`
|
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Optima Dev Skills For Codex
|
|
2
2
|
|
|
3
|
-
This repository provides shared development skills and CLI helpers for Optima engineers across `ci`, `stage`, and `prod
|
|
3
|
+
This repository provides shared development skills and CLI helpers for Optima engineers across `ci`, `stage`, `prod`, `cn-stage`, and `cn-prod` (Aliyun).
|
|
4
4
|
|
|
5
5
|
## Primary Entry Points
|
|
6
6
|
|
|
@@ -51,9 +51,9 @@ Some flows also require:
|
|
|
51
51
|
|
|
52
52
|
## Safety Rules
|
|
53
53
|
|
|
54
|
-
- Default to `ci` or `stage` when possible
|
|
55
|
-
- Treat `prod` as read-only unless the user explicitly asks for a write action.
|
|
56
|
-
- For `prod`, prefer limited queries and targeted operational commands.
|
|
54
|
+
- Default to `ci` or `stage` when possible; on the Aliyun side, default to `cn-stage`.
|
|
55
|
+
- Treat `prod` and `cn-prod` as read-only unless the user explicitly asks for a write action — `cn-prod` holds real production user data.
|
|
56
|
+
- For `prod` / `cn-prod`, prefer limited queries and targeted operational commands.
|
|
57
57
|
- Do not expose secrets unless the user explicitly asks to inspect them.
|
|
58
58
|
|
|
59
59
|
## Platform Notes
|
package/bin/helpers/query-db.ts
CHANGED
|
@@ -150,20 +150,111 @@ function queryDatabase(host: string, port: number, user: string, password: strin
|
|
|
150
150
|
return result;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const USAGE = `Usage: optima-query-db <service> "<sql>" [environment]
|
|
154
|
+
or: optima-query-db <service> "<sql>" --env <environment>
|
|
155
|
+
|
|
156
|
+
Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel
|
|
157
|
+
Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)
|
|
158
|
+
|
|
159
|
+
Example: optima-query-db user-auth "SELECT COUNT(*) FROM users" prod`;
|
|
160
|
+
|
|
161
|
+
/** 参数用法错误(入口打印 message + usage 后退非零,区别于运行期错误)。 */
|
|
162
|
+
export class QueryDbUsageError extends Error {}
|
|
163
|
+
|
|
164
|
+
const VALID_ENVS = ['ci', 'stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
165
|
+
// 'cn' 是历史别名(isCnEnv 认作 cn-prod),继续放行但不在 usage 里宣传。
|
|
166
|
+
const ENV_ALIASES = ['cn'];
|
|
167
|
+
|
|
168
|
+
// 单 token 旗标形态,含 --flag=value / 下划线(--env=cn-stage、--dry_run)。
|
|
169
|
+
// 带空白的 SQL 文本不会匹配;整段是注释的 SQL 由下方空语句检查兜住。
|
|
170
|
+
const FLAG_RE = /^--?[A-Za-z][-A-Za-z0-9_]*(=.*)?$/;
|
|
171
|
+
|
|
172
|
+
// SQL 去掉块注释、行注释、分号与空白后是否不剩任何语句(psql 对纯注释静默 no-op + exit 0)。
|
|
173
|
+
// 已知残留:PostgreSQL 块注释可嵌套(/* a /* b …两层闭合… ),本检查非嵌套感知会放行——
|
|
174
|
+
// 现实中几乎不出现,真出现也只是退回旧的空输出行为,不误伤合法 SQL。
|
|
175
|
+
function isEffectivelyEmptySql(sql: string): boolean {
|
|
176
|
+
const stripped = sql
|
|
177
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
178
|
+
.split('\n').map((l) => l.replace(/--.*/, '')).join('\n')
|
|
179
|
+
.replace(/[;\s]+/g, '');
|
|
180
|
+
return stripped.length === 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 解析 `<service> <sql> [environment]`,environment 也可经 --env/-e 旗标给
|
|
185
|
+
* (对齐 optima-logs 惯例)。历史 footgun(#60):`--env cn-stage` 曾被吞成
|
|
186
|
+
* sql='--env'(SQL 注释 = no-op)+ 真 SQL 静默丢弃,空输出 + exit 0 被误判成
|
|
187
|
+
* 「环境读不到」。故未知旗标、多余参数、未知环境、纯注释 SQL 一律硬报错。
|
|
188
|
+
*/
|
|
189
|
+
export function parseQueryDbArgs(
|
|
190
|
+
args: string[],
|
|
191
|
+
): { service: string; sql: string; environment: string } {
|
|
192
|
+
const positionals: string[] = [];
|
|
193
|
+
let envFromFlag: string | undefined;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < args.length; i++) {
|
|
196
|
+
const arg = args[i];
|
|
197
|
+
if (arg === '--env' || arg === '-e' || arg.startsWith('--env=') || arg.startsWith('-e=')) {
|
|
198
|
+
let value: string | undefined;
|
|
199
|
+
if (arg.includes('=')) {
|
|
200
|
+
value = arg.slice(arg.indexOf('=') + 1);
|
|
201
|
+
} else {
|
|
202
|
+
value = args[i + 1];
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
if (!value || FLAG_RE.test(value)) {
|
|
206
|
+
throw new QueryDbUsageError('--env 需要环境名,如 --env cn-stage');
|
|
207
|
+
}
|
|
208
|
+
if (envFromFlag !== undefined) {
|
|
209
|
+
throw new QueryDbUsageError('--env 重复指定');
|
|
210
|
+
}
|
|
211
|
+
envFromFlag = value;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (FLAG_RE.test(arg)) {
|
|
215
|
+
throw new QueryDbUsageError(`未知旗标 ${arg}(仅支持 --env <环境>,其余都是位置参数)`);
|
|
216
|
+
}
|
|
217
|
+
positionals.push(arg);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (positionals.length < 2) {
|
|
221
|
+
throw new QueryDbUsageError('缺少参数:需要 <service> <sql>');
|
|
222
|
+
}
|
|
223
|
+
const maxPositionals = envFromFlag !== undefined ? 2 : 3;
|
|
224
|
+
if (positionals.length > maxPositionals) {
|
|
225
|
+
throw new QueryDbUsageError(
|
|
226
|
+
envFromFlag !== undefined && positionals.length === 3
|
|
227
|
+
? `environment 同时以 --env 和位置参数给出:${envFromFlag} vs ${positionals[2]}`
|
|
228
|
+
: `多余参数已拒绝(绝不静默丢弃):${positionals.slice(maxPositionals).join(' ')}。SQL 含空格时记得整体加引号`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const [service, sql] = positionals;
|
|
233
|
+
const environment = envFromFlag ?? positionals[2] ?? 'ci';
|
|
234
|
+
|
|
235
|
+
if (!VALID_ENVS.includes(environment) && !ENV_ALIASES.includes(environment)) {
|
|
236
|
+
throw new QueryDbUsageError(`未知环境 ${environment}(可选 ${VALID_ENVS.join(' | ')})`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isEffectivelyEmptySql(sql)) {
|
|
240
|
+
throw new QueryDbUsageError(
|
|
241
|
+
'SQL 为空或全是注释/分号(psql 会静默 no-op)——检查参数顺序:<service> <sql> [environment]',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { service, sql, environment };
|
|
246
|
+
}
|
|
247
|
+
|
|
153
248
|
async function main() {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.
|
|
159
|
-
|
|
160
|
-
console.error('Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)');
|
|
161
|
-
console.error('');
|
|
162
|
-
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
163
|
-
process.exit(1);
|
|
249
|
+
const argv = process.argv.slice(2);
|
|
250
|
+
// help 只认第 1 个参数位:尾部混入的 -h 走未知旗标报错(exit 1),
|
|
251
|
+
// 不给「打 usage 后 exit 0 但没跑 SQL」的静默通道。
|
|
252
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
253
|
+
console.log(USAGE);
|
|
254
|
+
return;
|
|
164
255
|
}
|
|
165
256
|
|
|
166
|
-
const
|
|
257
|
+
const { service, sql, environment } = parseQueryDbArgs(argv);
|
|
167
258
|
|
|
168
259
|
if (!SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP]) {
|
|
169
260
|
console.error(`Unknown service: ${service}`);
|
|
@@ -273,7 +364,16 @@ async function main() {
|
|
|
273
364
|
}
|
|
274
365
|
}
|
|
275
366
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
367
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
368
|
+
// unit tests for parseQueryDbArgs) must not trigger main().
|
|
369
|
+
if (require.main === module) {
|
|
370
|
+
main().catch(error => {
|
|
371
|
+
if (error instanceof QueryDbUsageError) {
|
|
372
|
+
console.error(`❌ ${error.message}\n`);
|
|
373
|
+
console.error(USAGE);
|
|
374
|
+
} else {
|
|
375
|
+
console.error('\n❌ Error:', error.message);
|
|
376
|
+
}
|
|
377
|
+
process.exit(1);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.QueryDbUsageError = void 0;
|
|
38
|
+
exports.parseQueryDbArgs = parseQueryDbArgs;
|
|
37
39
|
const child_process_1 = require("child_process");
|
|
38
40
|
const fs = __importStar(require("fs"));
|
|
39
41
|
const db_utils_1 = require("./db-utils");
|
|
@@ -164,18 +166,95 @@ function queryDatabase(host, port, user, password, database, sql) {
|
|
|
164
166
|
});
|
|
165
167
|
return result;
|
|
166
168
|
}
|
|
169
|
+
const USAGE = `Usage: optima-query-db <service> "<sql>" [environment]
|
|
170
|
+
or: optima-query-db <service> "<sql>" --env <environment>
|
|
171
|
+
|
|
172
|
+
Services: commerce-backend, user-auth, agentic-chat, bi-backend, session-gateway, gateway-core, optima-logistics, billing, ads-backend, amazon-backend, browser-backend, shopify-backend, optima-generation, optima-sentinel
|
|
173
|
+
Environments: ci (default), stage, prod, cn-prod (阿里云生产), cn-stage (阿里云预发)
|
|
174
|
+
|
|
175
|
+
Example: optima-query-db user-auth "SELECT COUNT(*) FROM users" prod`;
|
|
176
|
+
/** 参数用法错误(入口打印 message + usage 后退非零,区别于运行期错误)。 */
|
|
177
|
+
class QueryDbUsageError extends Error {
|
|
178
|
+
}
|
|
179
|
+
exports.QueryDbUsageError = QueryDbUsageError;
|
|
180
|
+
const VALID_ENVS = ['ci', 'stage', 'prod', 'cn-prod', 'cn-stage'];
|
|
181
|
+
// 'cn' 是历史别名(isCnEnv 认作 cn-prod),继续放行但不在 usage 里宣传。
|
|
182
|
+
const ENV_ALIASES = ['cn'];
|
|
183
|
+
// 单 token 旗标形态,含 --flag=value / 下划线(--env=cn-stage、--dry_run)。
|
|
184
|
+
// 带空白的 SQL 文本不会匹配;整段是注释的 SQL 由下方空语句检查兜住。
|
|
185
|
+
const FLAG_RE = /^--?[A-Za-z][-A-Za-z0-9_]*(=.*)?$/;
|
|
186
|
+
// SQL 去掉块注释、行注释、分号与空白后是否不剩任何语句(psql 对纯注释静默 no-op + exit 0)。
|
|
187
|
+
// 已知残留:PostgreSQL 块注释可嵌套(/* a /* b …两层闭合… ),本检查非嵌套感知会放行——
|
|
188
|
+
// 现实中几乎不出现,真出现也只是退回旧的空输出行为,不误伤合法 SQL。
|
|
189
|
+
function isEffectivelyEmptySql(sql) {
|
|
190
|
+
const stripped = sql
|
|
191
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
192
|
+
.split('\n').map((l) => l.replace(/--.*/, '')).join('\n')
|
|
193
|
+
.replace(/[;\s]+/g, '');
|
|
194
|
+
return stripped.length === 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 解析 `<service> <sql> [environment]`,environment 也可经 --env/-e 旗标给
|
|
198
|
+
* (对齐 optima-logs 惯例)。历史 footgun(#60):`--env cn-stage` 曾被吞成
|
|
199
|
+
* sql='--env'(SQL 注释 = no-op)+ 真 SQL 静默丢弃,空输出 + exit 0 被误判成
|
|
200
|
+
* 「环境读不到」。故未知旗标、多余参数、未知环境、纯注释 SQL 一律硬报错。
|
|
201
|
+
*/
|
|
202
|
+
function parseQueryDbArgs(args) {
|
|
203
|
+
const positionals = [];
|
|
204
|
+
let envFromFlag;
|
|
205
|
+
for (let i = 0; i < args.length; i++) {
|
|
206
|
+
const arg = args[i];
|
|
207
|
+
if (arg === '--env' || arg === '-e' || arg.startsWith('--env=') || arg.startsWith('-e=')) {
|
|
208
|
+
let value;
|
|
209
|
+
if (arg.includes('=')) {
|
|
210
|
+
value = arg.slice(arg.indexOf('=') + 1);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
value = args[i + 1];
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
if (!value || FLAG_RE.test(value)) {
|
|
217
|
+
throw new QueryDbUsageError('--env 需要环境名,如 --env cn-stage');
|
|
218
|
+
}
|
|
219
|
+
if (envFromFlag !== undefined) {
|
|
220
|
+
throw new QueryDbUsageError('--env 重复指定');
|
|
221
|
+
}
|
|
222
|
+
envFromFlag = value;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (FLAG_RE.test(arg)) {
|
|
226
|
+
throw new QueryDbUsageError(`未知旗标 ${arg}(仅支持 --env <环境>,其余都是位置参数)`);
|
|
227
|
+
}
|
|
228
|
+
positionals.push(arg);
|
|
229
|
+
}
|
|
230
|
+
if (positionals.length < 2) {
|
|
231
|
+
throw new QueryDbUsageError('缺少参数:需要 <service> <sql>');
|
|
232
|
+
}
|
|
233
|
+
const maxPositionals = envFromFlag !== undefined ? 2 : 3;
|
|
234
|
+
if (positionals.length > maxPositionals) {
|
|
235
|
+
throw new QueryDbUsageError(envFromFlag !== undefined && positionals.length === 3
|
|
236
|
+
? `environment 同时以 --env 和位置参数给出:${envFromFlag} vs ${positionals[2]}`
|
|
237
|
+
: `多余参数已拒绝(绝不静默丢弃):${positionals.slice(maxPositionals).join(' ')}。SQL 含空格时记得整体加引号`);
|
|
238
|
+
}
|
|
239
|
+
const [service, sql] = positionals;
|
|
240
|
+
const environment = envFromFlag ?? positionals[2] ?? 'ci';
|
|
241
|
+
if (!VALID_ENVS.includes(environment) && !ENV_ALIASES.includes(environment)) {
|
|
242
|
+
throw new QueryDbUsageError(`未知环境 ${environment}(可选 ${VALID_ENVS.join(' | ')})`);
|
|
243
|
+
}
|
|
244
|
+
if (isEffectivelyEmptySql(sql)) {
|
|
245
|
+
throw new QueryDbUsageError('SQL 为空或全是注释/分号(psql 会静默 no-op)——检查参数顺序:<service> <sql> [environment]');
|
|
246
|
+
}
|
|
247
|
+
return { service, sql, environment };
|
|
248
|
+
}
|
|
167
249
|
async function main() {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.
|
|
173
|
-
|
|
174
|
-
console.error('');
|
|
175
|
-
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
176
|
-
process.exit(1);
|
|
250
|
+
const argv = process.argv.slice(2);
|
|
251
|
+
// help 只认第 1 个参数位:尾部混入的 -h 走未知旗标报错(exit 1),
|
|
252
|
+
// 不给「打 usage 后 exit 0 但没跑 SQL」的静默通道。
|
|
253
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
254
|
+
console.log(USAGE);
|
|
255
|
+
return;
|
|
177
256
|
}
|
|
178
|
-
const
|
|
257
|
+
const { service, sql, environment } = parseQueryDbArgs(argv);
|
|
179
258
|
if (!SERVICE_DB_MAP[service]) {
|
|
180
259
|
console.error(`Unknown service: ${service}`);
|
|
181
260
|
console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
|
|
@@ -267,7 +346,17 @@ async function main() {
|
|
|
267
346
|
console.log('\n' + result);
|
|
268
347
|
}
|
|
269
348
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
349
|
+
// Only run the CLI flow when invoked directly — being require()'d (e.g. by the
|
|
350
|
+
// unit tests for parseQueryDbArgs) must not trigger main().
|
|
351
|
+
if (require.main === module) {
|
|
352
|
+
main().catch(error => {
|
|
353
|
+
if (error instanceof QueryDbUsageError) {
|
|
354
|
+
console.error(`❌ ${error.message}\n`);
|
|
355
|
+
console.error(USAGE);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.error('\n❌ Error:', error.message);
|
|
359
|
+
}
|
|
360
|
+
process.exit(1);
|
|
361
|
+
});
|
|
362
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"postinstall": "node scripts/install.js",
|
|
25
25
|
"build": "tsc",
|
|
26
|
-
"prepare": "npm run build"
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"pretest": "npm run build",
|
|
28
|
+
"test": "node --test tests/*.test.js"
|
|
27
29
|
},
|
|
28
30
|
"keywords": [
|
|
29
31
|
"claude-code",
|
|
@@ -63,4 +65,4 @@
|
|
|
63
65
|
"ts-node": "^10.9.2",
|
|
64
66
|
"typescript": "^5.9.3"
|
|
65
67
|
}
|
|
66
|
-
}
|
|
68
|
+
}
|