@optima-chat/dev-skills 0.3.0 → 0.5.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/.claude/commands/query-db.md +189 -25
- package/.claude/settings.local.json +6 -1
- package/.claude/skills/query-db/SKILL.md +69 -53
- package/README.md +27 -4
- package/bin/helpers/query-db.ts +190 -0
- package/dist/bin/helpers/query-db.js +167 -0
- package/package.json +15 -5
- package/tsconfig.json +17 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
执行 SQL 查询,支持 CI/Stage/Prod 三个环境。
|
|
4
4
|
|
|
5
|
-
**版本**: v0.
|
|
5
|
+
**版本**: v0.5.0
|
|
6
6
|
|
|
7
7
|
## 使用场景
|
|
8
8
|
|
|
@@ -10,6 +10,29 @@
|
|
|
10
10
|
**调试**: 检查数据库状态、排查数据问题
|
|
11
11
|
**运维**: 查看生产数据、统计分析
|
|
12
12
|
|
|
13
|
+
## 🎯 推荐方式:使用 CLI 工具
|
|
14
|
+
|
|
15
|
+
**最简单的方式**是使用 `optima-query-db` CLI 工具,它会自动处理所有连接细节:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 查询 CI 环境(默认)
|
|
19
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM products"
|
|
20
|
+
|
|
21
|
+
# 查询 Stage 环境
|
|
22
|
+
optima-query-db user-auth "SELECT COUNT(*) FROM users" stage
|
|
23
|
+
|
|
24
|
+
# 查询 Prod 环境
|
|
25
|
+
optima-query-db commerce-backend "SELECT * FROM products LIMIT 5" prod
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**优点**:
|
|
29
|
+
- ✅ 自动管理 SSH 隧道
|
|
30
|
+
- ✅ 自动从 Infisical 获取密钥
|
|
31
|
+
- ✅ 无需手动执行多个步骤
|
|
32
|
+
- ✅ 支持所有环境
|
|
33
|
+
|
|
34
|
+
如果 CLI 工具不可用,可以使用下面的手动方式。
|
|
35
|
+
|
|
13
36
|
## 用法
|
|
14
37
|
|
|
15
38
|
```
|
|
@@ -45,10 +68,12 @@
|
|
|
45
68
|
|
|
46
69
|
## Claude Code 执行步骤
|
|
47
70
|
|
|
48
|
-
|
|
71
|
+
**首选方法**:使用 `optima-query-db` CLI 工具(见上方)
|
|
72
|
+
|
|
73
|
+
**备用方法**:如果 CLI 工具不可用,根据用户指定的 `environment` 参数选择执行方式:
|
|
49
74
|
- `ci` 或未指定 → 通过 SSH 连接 Docker Postgres(第 0 节,默认)
|
|
50
|
-
- `stage` → 通过
|
|
51
|
-
- `prod` → 通过
|
|
75
|
+
- `stage` → 通过 SSH 隧道访问 RDS(第 1 节)
|
|
76
|
+
- `prod` → 通过 SSH 隧道访问 RDS(第 2 节)
|
|
52
77
|
|
|
53
78
|
### 0. CI 环境(environment = "ci" 或默认)
|
|
54
79
|
|
|
@@ -114,47 +139,186 @@ sshpass -p "$CI_PASSWORD" ssh -o StrictHostKeyChecking=no ${CI_USER}@${CI_HOST}
|
|
|
114
139
|
|
|
115
140
|
### 1. Stage 环境(environment = "stage")
|
|
116
141
|
|
|
117
|
-
**访问方式**:
|
|
142
|
+
**访问方式**: 通过 EC2 SSH 隧道访问 RDS(通过 Infisical 获取密钥)
|
|
143
|
+
|
|
144
|
+
**前置条件**:
|
|
145
|
+
1. 获取 `optima-ec2-key` SSH 密钥文件(联系 xbfool)
|
|
146
|
+
2. 保存到 `~/.ssh/optima-ec2-key` 并设置权限: `chmod 600 ~/.ssh/optima-ec2-key`
|
|
118
147
|
|
|
119
148
|
**步骤**:
|
|
120
149
|
```bash
|
|
121
150
|
# IMPORTANT: 使用单行命令
|
|
122
151
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
# 1. 获取 Infisical 配置
|
|
153
|
+
INFISICAL_URL=$(gh variable get INFISICAL_URL -R Optima-Chat/optima-dev-skills)
|
|
154
|
+
INFISICAL_CLIENT_ID=$(gh variable get INFISICAL_CLIENT_ID -R Optima-Chat/optima-dev-skills)
|
|
155
|
+
INFISICAL_CLIENT_SECRET=$(gh variable get INFISICAL_CLIENT_SECRET -R Optima-Chat/optima-dev-skills)
|
|
156
|
+
INFISICAL_PROJECT_ID=$(gh variable get INFISICAL_PROJECT_ID -R Optima-Chat/optima-dev-skills)
|
|
157
|
+
|
|
158
|
+
# 2. 获取 Infisical Access Token
|
|
159
|
+
INFISICAL_TOKEN=$(curl -s -X POST "${INFISICAL_URL}/api/v1/auth/universal-auth/login" \
|
|
160
|
+
-H "Content-Type: application/json" \
|
|
161
|
+
-d "{\"clientId\": \"${INFISICAL_CLIENT_ID}\", \"clientSecret\": \"${INFISICAL_CLIENT_SECRET}\"}" \
|
|
162
|
+
| python3 -c "import sys, json; print(json.load(sys.stdin)['accessToken'])")
|
|
163
|
+
|
|
164
|
+
# 3. 从 Infisical 获取数据库配置(以 commerce-backend 为例)
|
|
165
|
+
curl -s "${INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${INFISICAL_PROJECT_ID}&environment=staging&secretPath=/infrastructure" \
|
|
166
|
+
-H "Authorization: Bearer ${INFISICAL_TOKEN}" | python3 -c "
|
|
167
|
+
import sys, json
|
|
168
|
+
secrets = {s['secretKey']: s['secretValue'] for s in json.load(sys.stdin)['secrets']}
|
|
169
|
+
print(f\"DATABASE_HOST={secrets['DATABASE_HOST']}\")
|
|
170
|
+
print(f\"COMMERCE_DB_USER={secrets['COMMERCE_DB_USER']}\")
|
|
171
|
+
print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
172
|
+
" > /tmp/stage_db_config.sh && source /tmp/stage_db_config.sh
|
|
173
|
+
|
|
174
|
+
# 4. 建立 SSH 隧道到 Stage EC2,通过隧道访问 RDS
|
|
175
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 15432:${DATABASE_HOST}:5432 ec2-user@54.179.132.102
|
|
176
|
+
|
|
177
|
+
# 5. 通过本地端口 15432 连接到 RDS
|
|
178
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 15432 -U "${COMMERCE_DB_USER}" -d optima_stage_commerce -c "SELECT COUNT(*) FROM products"
|
|
179
|
+
|
|
180
|
+
# 6. 关闭 SSH 隧道(可选)
|
|
181
|
+
pkill -f "ssh.*15432:${DATABASE_HOST}:5432"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**完整示例(四个服务)**:
|
|
185
|
+
```bash
|
|
186
|
+
# commerce-backend
|
|
187
|
+
# 使用 COMMERCE_DB_USER, COMMERCE_DB_PASSWORD, 数据库: optima_stage_commerce
|
|
188
|
+
|
|
189
|
+
# user-auth
|
|
190
|
+
# 使用 AUTH_DB_USER, AUTH_DB_PASSWORD, 数据库: optima_stage_auth
|
|
191
|
+
|
|
192
|
+
# mcp-host
|
|
193
|
+
# 使用 MCP_DB_USER, MCP_DB_PASSWORD, 数据库: optima_stage_mcp
|
|
126
194
|
|
|
127
|
-
#
|
|
128
|
-
|
|
195
|
+
# agentic-chat
|
|
196
|
+
# 使用 CHAT_DB_USER, CHAT_DB_PASSWORD, 数据库: optima_stage_chat
|
|
129
197
|
```
|
|
130
198
|
|
|
131
|
-
|
|
132
|
-
- `
|
|
133
|
-
- `
|
|
134
|
-
-
|
|
199
|
+
**数据库配置映射**:
|
|
200
|
+
- `commerce-backend`:
|
|
201
|
+
- 数据库: `optima_stage_commerce`
|
|
202
|
+
- 用户: Infisical `COMMERCE_DB_USER`
|
|
203
|
+
- 密码: Infisical `COMMERCE_DB_PASSWORD`
|
|
204
|
+
|
|
205
|
+
- `user-auth`:
|
|
206
|
+
- 数据库: `optima_stage_auth`
|
|
207
|
+
- 用户: Infisical `AUTH_DB_USER`
|
|
208
|
+
- 密码: Infisical `AUTH_DB_PASSWORD`
|
|
209
|
+
|
|
210
|
+
- `mcp-host`:
|
|
211
|
+
- 数据库: `optima_stage_mcp`
|
|
212
|
+
- 用户: Infisical `MCP_DB_USER`
|
|
213
|
+
- 密码: Infisical `MCP_DB_PASSWORD`
|
|
214
|
+
|
|
215
|
+
- `agentic-chat`:
|
|
216
|
+
- 数据库: `optima_stage_chat`
|
|
217
|
+
- 用户: Infisical `CHAT_DB_USER`
|
|
218
|
+
- 密码: Infisical `CHAT_DB_PASSWORD`
|
|
219
|
+
|
|
220
|
+
**说明**:
|
|
221
|
+
- Infisical 配置从 GitHub Variables 获取
|
|
222
|
+
- 数据库密钥从 Infisical 动态获取(项目: optima-secrets, 环境: staging, 路径: /infrastructure)
|
|
223
|
+
- DATABASE_HOST: `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
224
|
+
- Stage EC2 IP: `54.179.132.102`
|
|
225
|
+
- SSH 隧道: 本地端口 `15432` → EC2 → RDS `5432`
|
|
226
|
+
- Stage 和 Prod 共享同一个 RDS 实例,通过不同的数据库名隔离
|
|
135
227
|
|
|
136
228
|
### 2. Prod 环境(environment = "prod")
|
|
137
229
|
|
|
138
|
-
**访问方式**:
|
|
230
|
+
**访问方式**: 通过 EC2 SSH 隧道访问 RDS(通过 Infisical 获取密钥)
|
|
231
|
+
|
|
232
|
+
**前置条件**:
|
|
233
|
+
1. 获取 `optima-ec2-key` SSH 密钥文件(联系 xbfool)
|
|
234
|
+
2. 保存到 `~/.ssh/optima-ec2-key` 并设置权限: `chmod 600 ~/.ssh/optima-ec2-key`
|
|
139
235
|
|
|
140
236
|
**步骤**:
|
|
141
237
|
```bash
|
|
142
238
|
# IMPORTANT: 使用单行命令
|
|
143
|
-
# ⚠️
|
|
239
|
+
# ⚠️ 生产环境谨慎操作
|
|
240
|
+
|
|
241
|
+
# 1. 获取 Infisical 配置
|
|
242
|
+
INFISICAL_URL=$(gh variable get INFISICAL_URL -R Optima-Chat/optima-dev-skills)
|
|
243
|
+
INFISICAL_CLIENT_ID=$(gh variable get INFISICAL_CLIENT_ID -R Optima-Chat/optima-dev-skills)
|
|
244
|
+
INFISICAL_CLIENT_SECRET=$(gh variable get INFISICAL_CLIENT_SECRET -R Optima-Chat/optima-dev-skills)
|
|
245
|
+
INFISICAL_PROJECT_ID=$(gh variable get INFISICAL_PROJECT_ID -R Optima-Chat/optima-dev-skills)
|
|
246
|
+
|
|
247
|
+
# 2. 获取 Infisical Access Token
|
|
248
|
+
INFISICAL_TOKEN=$(curl -s -X POST "${INFISICAL_URL}/api/v1/auth/universal-auth/login" \
|
|
249
|
+
-H "Content-Type: application/json" \
|
|
250
|
+
-d "{\"clientId\": \"${INFISICAL_CLIENT_ID}\", \"clientSecret\": \"${INFISICAL_CLIENT_SECRET}\"}" \
|
|
251
|
+
| python3 -c "import sys, json; print(json.load(sys.stdin)['accessToken'])")
|
|
252
|
+
|
|
253
|
+
# 3. 从 Infisical 获取数据库配置(以 commerce-backend 为例)
|
|
254
|
+
curl -s "${INFISICAL_URL}/api/v3/secrets/raw?workspaceId=${INFISICAL_PROJECT_ID}&environment=prod&secretPath=/infrastructure" \
|
|
255
|
+
-H "Authorization: Bearer ${INFISICAL_TOKEN}" | python3 -c "
|
|
256
|
+
import sys, json
|
|
257
|
+
secrets = {s['secretKey']: s['secretValue'] for s in json.load(sys.stdin)['secrets']}
|
|
258
|
+
print(f\"DATABASE_HOST={secrets['DATABASE_HOST']}\")
|
|
259
|
+
print(f\"COMMERCE_DB_USER={secrets['COMMERCE_DB_USER']}\")
|
|
260
|
+
print(f\"COMMERCE_DB_PASSWORD={secrets['COMMERCE_DB_PASSWORD']}\")
|
|
261
|
+
" > /tmp/prod_db_config.sh && source /tmp/prod_db_config.sh
|
|
262
|
+
|
|
263
|
+
# 4. 建立 SSH 隧道到 Prod EC2,通过隧道访问 RDS
|
|
264
|
+
ssh -i ~/.ssh/optima-ec2-key -f -N -L 15433:${DATABASE_HOST}:5432 ec2-user@18.136.25.239
|
|
265
|
+
|
|
266
|
+
# 5. 通过本地端口 15433 连接到 RDS
|
|
267
|
+
PGPASSWORD="${COMMERCE_DB_PASSWORD}" psql -h localhost -p 15433 -U "${COMMERCE_DB_USER}" -d optima_commerce -c "SELECT COUNT(*) FROM products"
|
|
268
|
+
|
|
269
|
+
# 6. 关闭 SSH 隧道(可选)
|
|
270
|
+
pkill -f "ssh.*15433:${DATABASE_HOST}:5432"
|
|
271
|
+
```
|
|
144
272
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
273
|
+
**完整示例(四个服务)**:
|
|
274
|
+
```bash
|
|
275
|
+
# commerce-backend
|
|
276
|
+
# 使用 COMMERCE_DB_USER, COMMERCE_DB_PASSWORD, 数据库: optima_commerce
|
|
277
|
+
|
|
278
|
+
# user-auth
|
|
279
|
+
# 使用 AUTH_DB_USER, AUTH_DB_PASSWORD, 数据库: optima_auth
|
|
280
|
+
|
|
281
|
+
# mcp-host
|
|
282
|
+
# 使用 MCP_DB_USER, MCP_DB_PASSWORD, 数据库: optima_mcp
|
|
148
283
|
|
|
149
|
-
#
|
|
150
|
-
|
|
284
|
+
# agentic-chat
|
|
285
|
+
# 使用 CHAT_DB_USER, CHAT_DB_PASSWORD, 数据库: optima_chat
|
|
151
286
|
```
|
|
152
287
|
|
|
288
|
+
**数据库配置映射**:
|
|
289
|
+
- `commerce-backend`:
|
|
290
|
+
- 数据库: `optima_commerce`
|
|
291
|
+
- 用户: Infisical `COMMERCE_DB_USER`
|
|
292
|
+
- 密码: Infisical `COMMERCE_DB_PASSWORD`
|
|
293
|
+
|
|
294
|
+
- `user-auth`:
|
|
295
|
+
- 数据库: `optima_auth`
|
|
296
|
+
- 用户: Infisical `AUTH_DB_USER`
|
|
297
|
+
- 密码: Infisical `AUTH_DB_PASSWORD`
|
|
298
|
+
|
|
299
|
+
- `mcp-host`:
|
|
300
|
+
- 数据库: `optima_mcp`
|
|
301
|
+
- 用户: Infisical `MCP_DB_USER`
|
|
302
|
+
- 密码: Infisical `MCP_DB_PASSWORD`
|
|
303
|
+
|
|
304
|
+
- `agentic-chat`:
|
|
305
|
+
- 数据库: `optima_chat`
|
|
306
|
+
- 用户: Infisical `CHAT_DB_USER`
|
|
307
|
+
- 密码: Infisical `CHAT_DB_PASSWORD`
|
|
308
|
+
|
|
309
|
+
**说明**:
|
|
310
|
+
- Infisical 配置从 GitHub Variables 获取
|
|
311
|
+
- 数据库密钥从 Infisical 动态获取(项目: optima-secrets, 环境: prod, 路径: /infrastructure)
|
|
312
|
+
- DATABASE_HOST: `optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com`
|
|
313
|
+
- Prod EC2 IP: `18.136.25.239`
|
|
314
|
+
- SSH 隧道: 本地端口 `15433` → EC2 → RDS `5432` (注意 Prod 用 15433,Stage 用 15432)
|
|
315
|
+
- Stage 和 Prod 共享同一个 RDS 实例,通过不同的数据库名隔离
|
|
316
|
+
|
|
153
317
|
**⚠️ 生产环境安全规则**:
|
|
154
|
-
1.
|
|
155
|
-
2.
|
|
156
|
-
3.
|
|
157
|
-
4.
|
|
318
|
+
1. **谨慎操作** - 生产数据库,避免误操作
|
|
319
|
+
2. **避免 DELETE/UPDATE** - 除非明确需要
|
|
320
|
+
3. **使用 LIMIT** - 防止查询过多数据
|
|
321
|
+
4. **不查敏感数据** - 避免查询密码、密钥等
|
|
158
322
|
|
|
159
323
|
## 安全注意事项
|
|
160
324
|
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
"Bash(npm install:*)",
|
|
24
24
|
"Bash(optima-dev-skills:*)",
|
|
25
25
|
"Bash(gh variable set:*)",
|
|
26
|
-
"Bash(npm publish:*)"
|
|
26
|
+
"Bash(npm publish:*)",
|
|
27
|
+
"Bash(curl -s \"https://secrets.optima.onl/api/v3/secrets/raw?workspaceId=f2415dc2-f79d-4e41-90bb-cd3d2631ec71&environment=staging&secretPath=/infrastructure\" -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiMjZiNmYxMmItZjNjYy00OTE2LTliMTItNWFiYzZhMTlmNGI0IiwiY2xpZW50U2VjcmV0SWQiOiIxZjRhM2I0Ni03NTZmLTQwNmItOGQ1OS0yODI0YjUzOGFlMmMiLCJpZGVudGl0eUFjY2Vzc1Rva2VuSWQiOiJlMDQ1Y2MyOC02Nzk5LTRhODYtYWEwNy02YTQ4NmZkYzczODgiLCJhdXRoVG9rZW5UeXBlIjoiaWRlbnRpdHlBY2Nlc3NUb2tlbiIsImlhdCI6MTc2MzkxMzAxNywiZXhwIjoxNzY2NTA1MDE3fQ.xkPyv9MwXKLg3t-h1C_6mHSV5-cFuuvHnrkOoaGtuaQ\")",
|
|
28
|
+
"Bash(python3:*)",
|
|
29
|
+
"Bash(curl -s \"https://secrets.optima.onl/api/v1/folders?workspaceId=f2415dc2-f79d-4e41-90bb-cd3d2631ec71&environment=prod\" -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiMjZiNmYxMmItZjNjYy00OTE2LTliMTItNWFiYzZhMTlmNGI0IiwiY2xpZW50U2VjcmV0SWQiOiIxZjRhM2I0Ni03NTZmLTQwNmItOGQ1OS0yODI0YjUzOGFlMmMiLCJpZGVudGl0eUFjY2Vzc1Rva2VuSWQiOiJlMDQ1Y2MyOC02Nzk5LTRhODYtYWEwNy02YTQ4NmZkYzczODgiLCJhdXRoVG9rZW5UeXBlIjoiaWRlbnRpdHlBY2Nlc3NUb2tlbiIsImlhdCI6MTc2MzkxMzAxNywiZXhwIjoxNzY2NTA1MDE3fQ.xkPyv9MwXKLg3t-h1C_6mHSV5-cFuuvHnrkOoaGtuaQ\")",
|
|
30
|
+
"Bash(curl -s \"https://secrets.optima.onl/api/v3/secrets/raw?workspaceId=f2415dc2-f79d-4e41-90bb-cd3d2631ec71&environment=prod&secretPath=/infrastructure\" -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiMjZiNmYxMmItZjNjYy00OTE2LTliMTItNWFiYzZhMTlmNGI0IiwiY2xpZW50U2VjcmV0SWQiOiIxZjRhM2I0Ni03NTZmLTQwNmItOGQ1OS0yODI0YjUzOGFlMmMiLCJpZGVudGl0eUFjY2Vzc1Rva2VuSWQiOiJlMDQ1Y2MyOC02Nzk5LTRhODYtYWEwNy02YTQ4NmZkYzczODgiLCJhdXRoVG9rZW5UeXBlIjoiaWRlbnRpdHlBY2Nlc3NUb2tlbiIsImlhdCI6MTc2MzkxMzAxNywiZXhwIjoxNzY2NTA1MDE3fQ.xkPyv9MwXKLg3t-h1C_6mHSV5-cFuuvHnrkOoaGtuaQ\")",
|
|
31
|
+
"Bash(gh api:*)"
|
|
27
32
|
],
|
|
28
33
|
"deny": [],
|
|
29
34
|
"ask": []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "query-db"
|
|
3
|
-
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod 三个环境的 commerce-backend、user-auth、mcp-host、agentic-chat
|
|
3
|
+
description: "当用户请求查询数据库、执行SQL、查看数据、统计数据、检查数据库、查询表、数据库查询时,使用此技能。支持 CI、Stage、Prod 三个环境的 commerce-backend、user-auth、mcp-host、agentic-chat 服务的数据库查询。优先使用 optima-query-db CLI 工具。"
|
|
4
4
|
allowed-tools: ["Bash", "SlashCommand"]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,6 +8,32 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
8
8
|
|
|
9
9
|
当你需要执行 SQL 查询检查数据时,使用这个场景。
|
|
10
10
|
|
|
11
|
+
## 🎯 推荐方式:使用 CLI 工具
|
|
12
|
+
|
|
13
|
+
**最简单的方式**是直接使用 `optima-query-db` CLI 工具:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
optima-query-db <service> "<sql>" [environment]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
这个工具会自动处理:
|
|
20
|
+
- ✅ 获取 Infisical 配置
|
|
21
|
+
- ✅ 获取数据库密钥
|
|
22
|
+
- ✅ 建立 SSH 隧道(Stage/Prod)
|
|
23
|
+
- ✅ 执行查询
|
|
24
|
+
|
|
25
|
+
**示例**:
|
|
26
|
+
```bash
|
|
27
|
+
# CI 环境(默认)
|
|
28
|
+
optima-query-db user-auth "SELECT COUNT(*) FROM users"
|
|
29
|
+
|
|
30
|
+
# Stage 环境
|
|
31
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM products" stage
|
|
32
|
+
|
|
33
|
+
# Prod 环境
|
|
34
|
+
optima-query-db user-auth "SELECT COUNT(*) FROM users" prod
|
|
35
|
+
```
|
|
36
|
+
|
|
11
37
|
## 🎯 适用情况
|
|
12
38
|
|
|
13
39
|
- 验证数据是否正确插入/更新
|
|
@@ -18,60 +44,48 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
18
44
|
|
|
19
45
|
## 🚀 快速操作
|
|
20
46
|
|
|
21
|
-
###
|
|
47
|
+
### 使用 CLI 工具(推荐)
|
|
22
48
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
```bash
|
|
50
|
+
# CI 环境(默认)
|
|
51
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM products"
|
|
52
|
+
optima-query-db user-auth "SELECT email FROM users LIMIT 5"
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
- 默认环境,不需要指定 `ci` 参数
|
|
31
|
-
- 通过 SSH + Docker Exec 访问
|
|
32
|
-
- 可以执行任何 SQL 语句
|
|
54
|
+
# Stage 环境
|
|
55
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
/query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
|
|
57
|
+
# Prod 环境
|
|
58
|
+
optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY status" prod
|
|
38
59
|
```
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
- 查询 Stage 预发布环境
|
|
42
|
-
- 通过 AWS RDS 直连
|
|
43
|
-
|
|
44
|
-
### 3. 查询 Prod 环境数据库
|
|
61
|
+
### 使用 Slash 命令(备用)
|
|
45
62
|
|
|
46
63
|
```
|
|
47
|
-
/query-db commerce-backend "SELECT
|
|
64
|
+
/query-db commerce-backend "SELECT COUNT(*) FROM products"
|
|
65
|
+
/query-db user-auth "SELECT COUNT(*) FROM users" stage
|
|
66
|
+
/query-db commerce-backend "SELECT * FROM products LIMIT 5" prod
|
|
48
67
|
```
|
|
49
68
|
|
|
50
|
-
**说明**:
|
|
51
|
-
- 查询生产环境数据库
|
|
52
|
-
- ⚠️ **只读查询**,不能修改数据
|
|
53
|
-
- 使用只读用户连接
|
|
54
|
-
|
|
55
69
|
**常用服务**:
|
|
56
70
|
- `commerce-backend` - 电商数据库
|
|
57
71
|
- `user-auth` - 用户认证数据库
|
|
58
72
|
- `mcp-host` - MCP 协调器数据库
|
|
59
73
|
- `agentic-chat` - AI 聊天数据库
|
|
60
74
|
|
|
61
|
-
###
|
|
75
|
+
### 常用查询示例
|
|
62
76
|
|
|
63
|
-
```
|
|
77
|
+
```bash
|
|
64
78
|
# 统计查询
|
|
65
|
-
|
|
79
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM products WHERE status='active'"
|
|
66
80
|
|
|
67
81
|
# 查看最新数据
|
|
68
|
-
|
|
82
|
+
optima-query-db user-auth "SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 10"
|
|
69
83
|
|
|
70
84
|
# 聚合统计
|
|
71
|
-
|
|
85
|
+
optima-query-db commerce-backend "SELECT status, COUNT(*) as count FROM orders GROUP BY status"
|
|
72
86
|
|
|
73
87
|
# 检查特定记录
|
|
74
|
-
|
|
88
|
+
optima-query-db user-auth "SELECT * FROM users WHERE email='user@example.com'"
|
|
75
89
|
```
|
|
76
90
|
|
|
77
91
|
## 📋 常见使用场景
|
|
@@ -79,20 +93,20 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
79
93
|
### 场景 1:验证新功能
|
|
80
94
|
|
|
81
95
|
**步骤**:
|
|
82
|
-
1.
|
|
83
|
-
2.
|
|
96
|
+
1. 创建数据后查询:`optima-query-db commerce-backend "SELECT * FROM products WHERE title='新商品'"`
|
|
97
|
+
2. 检查关联数据:`optima-query-db commerce-backend "SELECT * FROM product_variants WHERE product_id=123"`
|
|
84
98
|
|
|
85
99
|
### 场景 2:数据统计
|
|
86
100
|
|
|
87
101
|
**步骤**:
|
|
88
|
-
1.
|
|
89
|
-
2.
|
|
102
|
+
1. 统计总数:`optima-query-db user-auth "SELECT COUNT(*) FROM users"`
|
|
103
|
+
2. 分组统计:`optima-query-db commerce-backend "SELECT DATE(created_at), COUNT(*) FROM orders GROUP BY DATE(created_at)"`
|
|
90
104
|
|
|
91
105
|
### 场景 3:排查问题
|
|
92
106
|
|
|
93
107
|
**步骤**:
|
|
94
|
-
1.
|
|
95
|
-
2.
|
|
108
|
+
1. 查找异常数据:`optima-query-db commerce-backend "SELECT * FROM orders WHERE status IS NULL"`
|
|
109
|
+
2. 检查重复数据:`optima-query-db user-auth "SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1"`
|
|
96
110
|
|
|
97
111
|
## ⚠️ 安全提示
|
|
98
112
|
|
|
@@ -105,14 +119,14 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
105
119
|
|
|
106
120
|
### 安全查询示例
|
|
107
121
|
|
|
108
|
-
```
|
|
122
|
+
```bash
|
|
109
123
|
# ✅ 好的查询
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM orders WHERE created_at > NOW() - INTERVAL '1 day'" prod
|
|
125
|
+
optima-query-db user-auth "SELECT id, email FROM users LIMIT 10" prod
|
|
112
126
|
|
|
113
127
|
# ❌ 不好的查询
|
|
114
|
-
#
|
|
115
|
-
#
|
|
128
|
+
# optima-query-db commerce-backend "SELECT * FROM orders" prod (全表扫描)
|
|
129
|
+
# optima-query-db user-auth "SELECT password_hash FROM users" prod (敏感数据)
|
|
116
130
|
```
|
|
117
131
|
|
|
118
132
|
## 💡 最佳实践
|
|
@@ -127,37 +141,39 @@ allowed-tools: ["Bash", "SlashCommand"]
|
|
|
127
141
|
|
|
128
142
|
### CI 环境
|
|
129
143
|
|
|
130
|
-
```
|
|
131
|
-
|
|
144
|
+
```bash
|
|
145
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM products"
|
|
132
146
|
```
|
|
133
147
|
|
|
134
148
|
**特点**:
|
|
135
149
|
- 开发环境,可以任意查询和修改
|
|
136
150
|
- 数据可以随时重置
|
|
137
|
-
- 通过 Docker 容器访问
|
|
151
|
+
- 通过 SSH + Docker 容器访问
|
|
138
152
|
|
|
139
153
|
### Stage 环境
|
|
140
154
|
|
|
141
|
-
```
|
|
142
|
-
|
|
155
|
+
```bash
|
|
156
|
+
optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
|
|
143
157
|
```
|
|
144
158
|
|
|
145
159
|
**特点**:
|
|
146
160
|
- 预发布环境
|
|
147
161
|
- 数据接近生产
|
|
148
|
-
- 通过
|
|
162
|
+
- 通过 SSH 隧道访问 RDS
|
|
149
163
|
|
|
150
164
|
### Prod 环境
|
|
151
165
|
|
|
152
|
-
```
|
|
153
|
-
|
|
166
|
+
```bash
|
|
167
|
+
optima-query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY status" prod
|
|
154
168
|
```
|
|
155
169
|
|
|
156
170
|
**特点**:
|
|
157
|
-
-
|
|
171
|
+
- 生产环境
|
|
158
172
|
- 真实用户数据
|
|
173
|
+
- 通过 SSH 隧道访问 RDS
|
|
159
174
|
- ⚠️ 谨慎使用
|
|
160
175
|
|
|
161
176
|
## 🔗 相关命令
|
|
162
177
|
|
|
163
|
-
-
|
|
178
|
+
- `optima-query-db` - CLI 查询工具(推荐)
|
|
179
|
+
- `/query-db` - Slash 命令(备用方式,详细使用方法请查看 `/query-db --help`)
|
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Claude:
|
|
|
72
72
|
|------|------|------|--------|
|
|
73
73
|
| `/logs` | 查看服务日志 | `/logs commerce-backend 100` | ✅ |
|
|
74
74
|
| `/query-db` | 查询数据库 | `/query-db user-auth "SELECT COUNT(*) FROM users"` | ✅ |
|
|
75
|
+
| `optima-query-db` | 数据库查询工具(CLI) | `optima-query-db user-auth "SELECT COUNT(*) FROM users" prod` | ✅ |
|
|
75
76
|
|
|
76
77
|
**说明**:
|
|
77
78
|
- 命令支持 CI、Stage、Prod 三个环境
|
|
@@ -99,7 +100,7 @@ optima-dev-skills/
|
|
|
99
100
|
|
|
100
101
|
## 💡 使用示例
|
|
101
102
|
|
|
102
|
-
###
|
|
103
|
+
### 示例 1:排查 Stage 环境问题
|
|
103
104
|
|
|
104
105
|
```
|
|
105
106
|
开发者: "Stage 的 /products API 返回 500"
|
|
@@ -113,6 +114,27 @@ Claude:
|
|
|
113
114
|
3. 问题定位:Stage RDS 连接配置问题
|
|
114
115
|
```
|
|
115
116
|
|
|
117
|
+
### 示例 2:使用 CLI 工具快速查询
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 查询 Prod 用户数
|
|
121
|
+
$ optima-query-db user-auth "SELECT COUNT(*) FROM users" prod
|
|
122
|
+
|
|
123
|
+
🔍 Querying user-auth (PROD)...
|
|
124
|
+
✓ Loaded Infisical config from GitHub Variables
|
|
125
|
+
✓ Obtained Infisical access token
|
|
126
|
+
✓ Retrieved database credentials from Infisical
|
|
127
|
+
✓ SSH tunnel established on port 15433
|
|
128
|
+
|
|
129
|
+
count
|
|
130
|
+
-------
|
|
131
|
+
26
|
|
132
|
+
(1 行记录)
|
|
133
|
+
|
|
134
|
+
# 查询 Stage 商品列表
|
|
135
|
+
$ optima-query-db commerce-backend "SELECT id, title FROM products LIMIT 5" stage
|
|
136
|
+
```
|
|
137
|
+
|
|
116
138
|
## 🎯 设计原则
|
|
117
139
|
|
|
118
140
|
### dev-skills 提供什么?
|
|
@@ -162,15 +184,16 @@ Claude:
|
|
|
162
184
|
|
|
163
185
|
## 🛠️ 开发状态
|
|
164
186
|
|
|
165
|
-
**当前版本**: 0.
|
|
187
|
+
**当前版本**: 0.5.0
|
|
166
188
|
|
|
167
189
|
**已完成**:
|
|
168
190
|
- ✅ 2 个跨环境命令:`/logs`、`/query-db`
|
|
169
191
|
- ✅ 2 个任务场景:`logs` skill、`query-db` skill
|
|
170
192
|
- ✅ 支持 CI、Stage、Prod 三个环境
|
|
171
193
|
- ✅ CI 环境通过 SSH + Docker 访问
|
|
172
|
-
- ✅ Stage/Prod 通过
|
|
173
|
-
- ✅
|
|
194
|
+
- ✅ Stage/Prod 通过 SSH 隧道访问 RDS
|
|
195
|
+
- ✅ TypeScript CLI 工具:`optima-query-db`
|
|
196
|
+
- ✅ 通过 Infisical 动态获取密钥
|
|
174
197
|
|
|
175
198
|
**设计原则**:
|
|
176
199
|
- 命令提供信息(URL、路径、凭证位置),不实现复杂逻辑
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
interface InfisicalConfig {
|
|
7
|
+
url: string;
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
projectId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface DatabaseConfig {
|
|
14
|
+
host: string;
|
|
15
|
+
user: string;
|
|
16
|
+
password: string;
|
|
17
|
+
database: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SERVICE_DB_MAP = {
|
|
21
|
+
'commerce-backend': {
|
|
22
|
+
ci: { container: 'commerce-postgres', user: 'commerce', password: 'commerce123', database: 'commerce' },
|
|
23
|
+
stage: { userKey: 'COMMERCE_DB_USER', passwordKey: 'COMMERCE_DB_PASSWORD', database: 'optima_stage_commerce' },
|
|
24
|
+
prod: { userKey: 'COMMERCE_DB_USER', passwordKey: 'COMMERCE_DB_PASSWORD', database: 'optima_commerce' }
|
|
25
|
+
},
|
|
26
|
+
'user-auth': {
|
|
27
|
+
ci: { container: 'user-auth-postgres-1', user: 'userauth', password: 'password123', database: 'userauth' },
|
|
28
|
+
stage: { userKey: 'AUTH_DB_USER', passwordKey: 'AUTH_DB_PASSWORD', database: 'optima_stage_auth' },
|
|
29
|
+
prod: { userKey: 'AUTH_DB_USER', passwordKey: 'AUTH_DB_PASSWORD', database: 'optima_auth' }
|
|
30
|
+
},
|
|
31
|
+
'mcp-host': {
|
|
32
|
+
ci: { container: 'mcp-host-db-1', user: 'mcp_user', password: 'mcp_password', database: 'mcp_host' },
|
|
33
|
+
stage: { userKey: 'MCP_DB_USER', passwordKey: 'MCP_DB_PASSWORD', database: 'optima_stage_mcp' },
|
|
34
|
+
prod: { userKey: 'MCP_DB_USER', passwordKey: 'MCP_DB_PASSWORD', database: 'optima_mcp' }
|
|
35
|
+
},
|
|
36
|
+
'agentic-chat': {
|
|
37
|
+
ci: { container: 'optima-postgres', user: 'postgres', password: 'postgres123', database: 'optima_chat' },
|
|
38
|
+
stage: { userKey: 'CHAT_DB_USER', passwordKey: 'CHAT_DB_PASSWORD', database: 'optima_stage_chat' },
|
|
39
|
+
prod: { userKey: 'CHAT_DB_USER', passwordKey: 'CHAT_DB_PASSWORD', database: 'optima_chat' }
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const EC2_HOSTS = {
|
|
44
|
+
stage: '54.179.132.102',
|
|
45
|
+
prod: '18.136.25.239'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getGitHubVariable(name: string): string {
|
|
49
|
+
return execSync(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getInfisicalConfig(): InfisicalConfig {
|
|
53
|
+
return {
|
|
54
|
+
url: getGitHubVariable('INFISICAL_URL'),
|
|
55
|
+
clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
|
|
56
|
+
clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
|
|
57
|
+
projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getInfisicalToken(config: InfisicalConfig): string {
|
|
62
|
+
const response = execSync(
|
|
63
|
+
`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`,
|
|
64
|
+
{ encoding: 'utf-8' }
|
|
65
|
+
);
|
|
66
|
+
return JSON.parse(response).accessToken;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getInfisicalSecrets(config: InfisicalConfig, token: string, environment: string): Record<string, string> {
|
|
70
|
+
const response = execSync(
|
|
71
|
+
`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=/infrastructure" -H "Authorization: Bearer ${token}"`,
|
|
72
|
+
{ encoding: 'utf-8' }
|
|
73
|
+
);
|
|
74
|
+
const data = JSON.parse(response);
|
|
75
|
+
const secrets: Record<string, string> = {};
|
|
76
|
+
for (const secret of data.secrets) {
|
|
77
|
+
secrets[secret.secretKey] = secret.secretValue;
|
|
78
|
+
}
|
|
79
|
+
return secrets;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setupSSHTunnel(ec2Host: string, dbHost: string, localPort: number): void {
|
|
83
|
+
// 检查是否已有隧道
|
|
84
|
+
try {
|
|
85
|
+
execSync(`lsof -ti:${localPort}`, { stdio: 'ignore' });
|
|
86
|
+
console.log(`✓ SSH tunnel already exists on port ${localPort}`);
|
|
87
|
+
return;
|
|
88
|
+
} catch {
|
|
89
|
+
// 端口未占用,创建隧道
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sshKeyPath = `${process.env.HOME}/.ssh/optima-ec2-key`;
|
|
93
|
+
if (!fs.existsSync(sshKeyPath)) {
|
|
94
|
+
throw new Error(`SSH key not found: ${sshKeyPath}. Please obtain optima-ec2-key from xbfool.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Creating SSH tunnel: localhost:${localPort} -> ${ec2Host} -> ${dbHost}:5432`);
|
|
98
|
+
execSync(
|
|
99
|
+
`ssh -i ${sshKeyPath} -f -N -o StrictHostKeyChecking=no -L ${localPort}:${dbHost}:5432 ec2-user@${ec2Host}`,
|
|
100
|
+
{ stdio: 'inherit' }
|
|
101
|
+
);
|
|
102
|
+
console.log(`✓ SSH tunnel established on port ${localPort}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function queryDatabase(host: string, port: number, user: string, password: string, database: string, sql: string): string {
|
|
106
|
+
const psqlPath = '/usr/local/opt/postgresql@16/bin/psql';
|
|
107
|
+
|
|
108
|
+
if (!fs.existsSync(psqlPath)) {
|
|
109
|
+
throw new Error('PostgreSQL client not found. Install with: brew install postgresql@16');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = execSync(
|
|
113
|
+
`PGPASSWORD="${password}" ${psqlPath} -h ${host} -p ${port} -U ${user} -d ${database} -c "${sql}"`,
|
|
114
|
+
{ encoding: 'utf-8' }
|
|
115
|
+
);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const args = process.argv.slice(2);
|
|
121
|
+
|
|
122
|
+
if (args.length < 2) {
|
|
123
|
+
console.error('Usage: query-db.ts <service> <sql> [environment]');
|
|
124
|
+
console.error('');
|
|
125
|
+
console.error('Services: commerce-backend, user-auth, mcp-host, agentic-chat');
|
|
126
|
+
console.error('Environments: ci (default), stage, prod');
|
|
127
|
+
console.error('');
|
|
128
|
+
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [service, sql, environment = 'ci'] = args;
|
|
133
|
+
|
|
134
|
+
if (!SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP]) {
|
|
135
|
+
console.error(`Unknown service: ${service}`);
|
|
136
|
+
console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const serviceConfig = SERVICE_DB_MAP[service as keyof typeof SERVICE_DB_MAP][environment as 'ci' | 'stage' | 'prod'];
|
|
141
|
+
|
|
142
|
+
console.log(`\n🔍 Querying ${service} (${environment.toUpperCase()})...`);
|
|
143
|
+
|
|
144
|
+
if (environment === 'ci') {
|
|
145
|
+
// CI 环境:通过 SSH + Docker Exec
|
|
146
|
+
const ciUser = getGitHubVariable('CI_SSH_USER');
|
|
147
|
+
const ciHost = getGitHubVariable('CI_SSH_HOST');
|
|
148
|
+
const ciPassword = getGitHubVariable('CI_SSH_PASSWORD');
|
|
149
|
+
|
|
150
|
+
const { container, user, database } = serviceConfig as any;
|
|
151
|
+
|
|
152
|
+
const result = execSync(
|
|
153
|
+
`sshpass -p "${ciPassword}" ssh -o StrictHostKeyChecking=no ${ciUser}@${ciHost} "docker exec ${container} psql -U ${user} -d ${database} -c \\"${sql}\\""`,
|
|
154
|
+
{ encoding: 'utf-8' }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
console.log('\n' + result);
|
|
158
|
+
} else {
|
|
159
|
+
// Stage/Prod 环境:通过 SSH 隧道访问 RDS
|
|
160
|
+
const infisicalConfig = getInfisicalConfig();
|
|
161
|
+
console.log('✓ Loaded Infisical config from GitHub Variables');
|
|
162
|
+
|
|
163
|
+
const token = getInfisicalToken(infisicalConfig);
|
|
164
|
+
console.log('✓ Obtained Infisical access token');
|
|
165
|
+
|
|
166
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, environment === 'stage' ? 'staging' : 'prod');
|
|
167
|
+
console.log('✓ Retrieved database credentials from Infisical');
|
|
168
|
+
|
|
169
|
+
const { userKey, passwordKey, database } = serviceConfig as any;
|
|
170
|
+
const dbHost = secrets.DATABASE_HOST;
|
|
171
|
+
const dbUser = secrets[userKey];
|
|
172
|
+
const dbPassword = secrets[passwordKey];
|
|
173
|
+
|
|
174
|
+
const localPort = environment === 'stage' ? 15432 : 15433;
|
|
175
|
+
const ec2Host = EC2_HOSTS[environment as 'stage' | 'prod'];
|
|
176
|
+
|
|
177
|
+
setupSSHTunnel(ec2Host, dbHost, localPort);
|
|
178
|
+
|
|
179
|
+
// 等待隧道建立
|
|
180
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
181
|
+
|
|
182
|
+
const result = queryDatabase('localhost', localPort, dbUser, dbPassword, database, sql);
|
|
183
|
+
console.log('\n' + result);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch(error => {
|
|
188
|
+
console.error('\n❌ Error:', error.message);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const SERVICE_DB_MAP = {
|
|
40
|
+
'commerce-backend': {
|
|
41
|
+
ci: { container: 'commerce-postgres', user: 'commerce', password: 'commerce123', database: 'commerce' },
|
|
42
|
+
stage: { userKey: 'COMMERCE_DB_USER', passwordKey: 'COMMERCE_DB_PASSWORD', database: 'optima_stage_commerce' },
|
|
43
|
+
prod: { userKey: 'COMMERCE_DB_USER', passwordKey: 'COMMERCE_DB_PASSWORD', database: 'optima_commerce' }
|
|
44
|
+
},
|
|
45
|
+
'user-auth': {
|
|
46
|
+
ci: { container: 'user-auth-postgres-1', user: 'userauth', password: 'password123', database: 'userauth' },
|
|
47
|
+
stage: { userKey: 'AUTH_DB_USER', passwordKey: 'AUTH_DB_PASSWORD', database: 'optima_stage_auth' },
|
|
48
|
+
prod: { userKey: 'AUTH_DB_USER', passwordKey: 'AUTH_DB_PASSWORD', database: 'optima_auth' }
|
|
49
|
+
},
|
|
50
|
+
'mcp-host': {
|
|
51
|
+
ci: { container: 'mcp-host-db-1', user: 'mcp_user', password: 'mcp_password', database: 'mcp_host' },
|
|
52
|
+
stage: { userKey: 'MCP_DB_USER', passwordKey: 'MCP_DB_PASSWORD', database: 'optima_stage_mcp' },
|
|
53
|
+
prod: { userKey: 'MCP_DB_USER', passwordKey: 'MCP_DB_PASSWORD', database: 'optima_mcp' }
|
|
54
|
+
},
|
|
55
|
+
'agentic-chat': {
|
|
56
|
+
ci: { container: 'optima-postgres', user: 'postgres', password: 'postgres123', database: 'optima_chat' },
|
|
57
|
+
stage: { userKey: 'CHAT_DB_USER', passwordKey: 'CHAT_DB_PASSWORD', database: 'optima_stage_chat' },
|
|
58
|
+
prod: { userKey: 'CHAT_DB_USER', passwordKey: 'CHAT_DB_PASSWORD', database: 'optima_chat' }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const EC2_HOSTS = {
|
|
62
|
+
stage: '54.179.132.102',
|
|
63
|
+
prod: '18.136.25.239'
|
|
64
|
+
};
|
|
65
|
+
function getGitHubVariable(name) {
|
|
66
|
+
return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
|
|
67
|
+
}
|
|
68
|
+
function getInfisicalConfig() {
|
|
69
|
+
return {
|
|
70
|
+
url: getGitHubVariable('INFISICAL_URL'),
|
|
71
|
+
clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
|
|
72
|
+
clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
|
|
73
|
+
projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function getInfisicalToken(config) {
|
|
77
|
+
const response = (0, child_process_1.execSync)(`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`, { encoding: 'utf-8' });
|
|
78
|
+
return JSON.parse(response).accessToken;
|
|
79
|
+
}
|
|
80
|
+
function getInfisicalSecrets(config, token, environment) {
|
|
81
|
+
const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=/infrastructure" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
|
|
82
|
+
const data = JSON.parse(response);
|
|
83
|
+
const secrets = {};
|
|
84
|
+
for (const secret of data.secrets) {
|
|
85
|
+
secrets[secret.secretKey] = secret.secretValue;
|
|
86
|
+
}
|
|
87
|
+
return secrets;
|
|
88
|
+
}
|
|
89
|
+
function setupSSHTunnel(ec2Host, dbHost, localPort) {
|
|
90
|
+
// 检查是否已有隧道
|
|
91
|
+
try {
|
|
92
|
+
(0, child_process_1.execSync)(`lsof -ti:${localPort}`, { stdio: 'ignore' });
|
|
93
|
+
console.log(`✓ SSH tunnel already exists on port ${localPort}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// 端口未占用,创建隧道
|
|
98
|
+
}
|
|
99
|
+
const sshKeyPath = `${process.env.HOME}/.ssh/optima-ec2-key`;
|
|
100
|
+
if (!fs.existsSync(sshKeyPath)) {
|
|
101
|
+
throw new Error(`SSH key not found: ${sshKeyPath}. Please obtain optima-ec2-key from xbfool.`);
|
|
102
|
+
}
|
|
103
|
+
console.log(`Creating SSH tunnel: localhost:${localPort} -> ${ec2Host} -> ${dbHost}:5432`);
|
|
104
|
+
(0, child_process_1.execSync)(`ssh -i ${sshKeyPath} -f -N -o StrictHostKeyChecking=no -L ${localPort}:${dbHost}:5432 ec2-user@${ec2Host}`, { stdio: 'inherit' });
|
|
105
|
+
console.log(`✓ SSH tunnel established on port ${localPort}`);
|
|
106
|
+
}
|
|
107
|
+
function queryDatabase(host, port, user, password, database, sql) {
|
|
108
|
+
const psqlPath = '/usr/local/opt/postgresql@16/bin/psql';
|
|
109
|
+
if (!fs.existsSync(psqlPath)) {
|
|
110
|
+
throw new Error('PostgreSQL client not found. Install with: brew install postgresql@16');
|
|
111
|
+
}
|
|
112
|
+
const result = (0, child_process_1.execSync)(`PGPASSWORD="${password}" ${psqlPath} -h ${host} -p ${port} -U ${user} -d ${database} -c "${sql}"`, { encoding: 'utf-8' });
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
async function main() {
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
if (args.length < 2) {
|
|
118
|
+
console.error('Usage: query-db.ts <service> <sql> [environment]');
|
|
119
|
+
console.error('');
|
|
120
|
+
console.error('Services: commerce-backend, user-auth, mcp-host, agentic-chat');
|
|
121
|
+
console.error('Environments: ci (default), stage, prod');
|
|
122
|
+
console.error('');
|
|
123
|
+
console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const [service, sql, environment = 'ci'] = args;
|
|
127
|
+
if (!SERVICE_DB_MAP[service]) {
|
|
128
|
+
console.error(`Unknown service: ${service}`);
|
|
129
|
+
console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const serviceConfig = SERVICE_DB_MAP[service][environment];
|
|
133
|
+
console.log(`\n🔍 Querying ${service} (${environment.toUpperCase()})...`);
|
|
134
|
+
if (environment === 'ci') {
|
|
135
|
+
// CI 环境:通过 SSH + Docker Exec
|
|
136
|
+
const ciUser = getGitHubVariable('CI_SSH_USER');
|
|
137
|
+
const ciHost = getGitHubVariable('CI_SSH_HOST');
|
|
138
|
+
const ciPassword = getGitHubVariable('CI_SSH_PASSWORD');
|
|
139
|
+
const { container, user, database } = serviceConfig;
|
|
140
|
+
const result = (0, child_process_1.execSync)(`sshpass -p "${ciPassword}" ssh -o StrictHostKeyChecking=no ${ciUser}@${ciHost} "docker exec ${container} psql -U ${user} -d ${database} -c \\"${sql}\\""`, { encoding: 'utf-8' });
|
|
141
|
+
console.log('\n' + result);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Stage/Prod 环境:通过 SSH 隧道访问 RDS
|
|
145
|
+
const infisicalConfig = getInfisicalConfig();
|
|
146
|
+
console.log('✓ Loaded Infisical config from GitHub Variables');
|
|
147
|
+
const token = getInfisicalToken(infisicalConfig);
|
|
148
|
+
console.log('✓ Obtained Infisical access token');
|
|
149
|
+
const secrets = getInfisicalSecrets(infisicalConfig, token, environment === 'stage' ? 'staging' : 'prod');
|
|
150
|
+
console.log('✓ Retrieved database credentials from Infisical');
|
|
151
|
+
const { userKey, passwordKey, database } = serviceConfig;
|
|
152
|
+
const dbHost = secrets.DATABASE_HOST;
|
|
153
|
+
const dbUser = secrets[userKey];
|
|
154
|
+
const dbPassword = secrets[passwordKey];
|
|
155
|
+
const localPort = environment === 'stage' ? 15432 : 15433;
|
|
156
|
+
const ec2Host = EC2_HOSTS[environment];
|
|
157
|
+
setupSSHTunnel(ec2Host, dbHost, localPort);
|
|
158
|
+
// 等待隧道建立
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
160
|
+
const result = queryDatabase('localhost', localPort, dbUser, dbPassword, database, sql);
|
|
161
|
+
console.log('\n' + result);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
main().catch(error => {
|
|
165
|
+
console.error('\n❌ Error:', error.message);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optima-chat/dev-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"optima-dev-skills": "./bin/cli.js"
|
|
7
|
+
"optima-dev-skills": "./bin/cli.js",
|
|
8
|
+
"optima-query-db": "./bin/helpers/query-db.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"postinstall": "node scripts/install.js"
|
|
11
|
+
"postinstall": "node scripts/install.js",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepare": "npm run build"
|
|
11
14
|
},
|
|
12
15
|
"keywords": [
|
|
13
16
|
"claude-code",
|
|
@@ -34,7 +37,14 @@
|
|
|
34
37
|
"files": [
|
|
35
38
|
".claude",
|
|
36
39
|
"bin",
|
|
40
|
+
"dist",
|
|
37
41
|
"scripts",
|
|
38
|
-
"README.md"
|
|
39
|
-
|
|
42
|
+
"README.md",
|
|
43
|
+
"tsconfig.json"
|
|
44
|
+
],
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^24.10.1",
|
|
47
|
+
"ts-node": "^10.9.2",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
40
50
|
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node"
|
|
14
|
+
},
|
|
15
|
+
"include": ["bin/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|