@optima-chat/dev-skills 0.4.0 → 0.5.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  执行 SQL 查询,支持 CI/Stage/Prod 三个环境。
4
4
 
5
- **版本**: v0.3.0
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
- **重要提示**:根据用户指定的 `environment` 参数选择执行方式:
71
+ **首选方法**:使用 `optima-query-db` CLI 工具(见上方)
72
+
73
+ **备用方法**:如果 CLI 工具不可用,根据用户指定的 `environment` 参数选择执行方式:
49
74
  - `ci` 或未指定 → 通过 SSH 连接 Docker Postgres(第 0 节,默认)
50
- - `stage` → 通过 AWS RDS 端点连接(第 1 节)
51
- - `prod` → 通过 AWS RDS 端点连接(第 2 节,⚠️ 只读)
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
- ### 1. 查询 CI 环境数据库(默认)
47
+ ### 使用 CLI 工具(推荐)
22
48
 
23
- ```
24
- /query-db commerce-backend "SELECT COUNT(*) FROM products"
25
- /query-db user-auth "SELECT email FROM users LIMIT 5"
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
- - 查询 CI 开发环境数据库
30
- - 默认环境,不需要指定 `ci` 参数
31
- - 通过 SSH + Docker Exec 访问
32
- - 可以执行任何 SQL 语句
54
+ # Stage 环境
55
+ optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
33
56
 
34
- ### 2. 查询 Stage 环境数据库
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 status, COUNT(*) FROM orders GROUP BY status" prod
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
- ### 4. 常用查询示例
75
+ ### 常用查询示例
62
76
 
63
- ```
77
+ ```bash
64
78
  # 统计查询
65
- /query-db commerce-backend "SELECT COUNT(*) FROM products WHERE status='active'"
79
+ optima-query-db commerce-backend "SELECT COUNT(*) FROM products WHERE status='active'"
66
80
 
67
81
  # 查看最新数据
68
- /query-db user-auth "SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 10"
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
- /query-db commerce-backend "SELECT status, COUNT(*) as count FROM orders GROUP BY status"
85
+ optima-query-db commerce-backend "SELECT status, COUNT(*) as count FROM orders GROUP BY status"
72
86
 
73
87
  # 检查特定记录
74
- /query-db user-auth "SELECT * FROM users WHERE email='user@example.com'"
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. 创建数据后查询:`/query-db commerce-backend "SELECT * FROM products WHERE title='新商品'"`
83
- 2. 检查关联数据:`/query-db commerce-backend "SELECT * FROM product_variants WHERE product_id=123"`
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. 统计总数:`/query-db user-auth "SELECT COUNT(*) FROM users"`
89
- 2. 分组统计:`/query-db commerce-backend "SELECT DATE(created_at), COUNT(*) FROM orders GROUP BY DATE(created_at)"`
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. 查找异常数据:`/query-db commerce-backend "SELECT * FROM orders WHERE status IS NULL"`
95
- 2. 检查重复数据:`/query-db user-auth "SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1"`
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
- /query-db commerce-backend "SELECT COUNT(*) FROM orders WHERE created_at > NOW() - INTERVAL '1 day'" prod
111
- /query-db user-auth "SELECT id, email FROM users LIMIT 10" prod
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
- # /query-db commerce-backend "SELECT * FROM orders" prod (全表扫描)
115
- # /query-db user-auth "SELECT password_hash FROM users" prod (敏感数据)
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
- /query-db commerce-backend "SELECT COUNT(*) FROM products"
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
- /query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
155
+ ```bash
156
+ optima-query-db commerce-backend "SELECT COUNT(*) FROM orders" stage
143
157
  ```
144
158
 
145
159
  **特点**:
146
160
  - 预发布环境
147
161
  - 数据接近生产
148
- - 通过 AWS RDS 访问
162
+ - 通过 SSH 隧道访问 RDS
149
163
 
150
164
  ### Prod 环境
151
165
 
152
- ```
153
- /query-db commerce-backend "SELECT status, COUNT(*) FROM orders GROUP BY status" prod
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
- - `/query-db` - 查询数据库(详细使用方法请查看 `/query-db --help`)
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
- ### 示例:排查 Stage 环境问题
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.3.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 通过 AWS 服务访问
173
- - ✅ 数据库查询支持只读模式(Prod)
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.4.0",
3
+ "version": "0.5.1",
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": "./dist/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
+ }