@optima-chat/dev-skills 0.4.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 +29 -4
- 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
|
|
|
@@ -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
|
+
}
|