@optima-chat/dev-skills 0.13.0 → 0.13.1

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