@optima-chat/dev-skills 0.12.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
 
@@ -28,6 +28,19 @@ optima-cn-deploy --list # 列出全部可发服务
28
28
  5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
29
29
  6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
30
30
 
31
+
32
+ ## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
33
+
34
+ ```bash
35
+ optima-cn-deploy <service> --vtag cn-v1.2.3 # 先 stage 同 tag 验证
36
+ optima-cn-deploy <service> --env prod --vtag cn-v1.2.3 # prod 发版
37
+ ```
38
+
39
+ - prod 是 vtag 制:必须先 `git tag cn-vX.Y.Z && git push`,工具拒绝无 vtag / 裸 v* / 带 --branch 的 prod 请求
40
+ - prod 流水线构建后**停在人工卡点**(xbfool/svenyang 在云效控制台审批),之后 DB 迁移 + digest 钉死部署自动走完
41
+ - 成功标准:prod SAE ImageUrl 为 `@sha256:` digest 寻址(工具最后一行校验)
42
+ - 用户请求发 cn-prod 时:确认已有 stage 验证过的 cn-v tag;没有则先引导走 stage
43
+
31
44
  ## 前置依赖
32
45
 
33
46
  - `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
@@ -35,5 +48,5 @@ optima-cn-deploy --list # 列出全部可发服务
35
48
 
36
49
  ## 边界
37
50
 
38
- - 只发 **cn-stage**,不发 cn-prod(cn-prod 发布另走人工卡点流程)。
51
+ - cn-prod 仅走 vtag 制(见上),且有人工卡点闸门;日常无 vtag 的构建只发 cn-stage。
39
52
  - 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
@@ -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
 
@@ -28,6 +28,19 @@ optima-cn-deploy --list # 列出全部可发服务
28
28
  5. 失败排查:命令输出的 flow.aliyun.com 链接里看具体 step 日志;常见原因是该分支/仓的 Codeup mirror 凭证问题(命令会在 mirror 步就报错并中止,不会构建旧代码)。
29
29
  6. 成功标准:最后一行 `✓ SAE ImageUrl tag=<sha>` —— 流水线绿且 SAE 真的钉到了目标 commit。只看流水线绿不算完成。
30
30
 
31
+
32
+ ## cn-prod vtag 发版(⚠️ 待 svenyang 首次验证通过后再日常使用)
33
+
34
+ ```bash
35
+ optima-cn-deploy <service> --vtag cn-v1.2.3 # 先 stage 同 tag 验证
36
+ optima-cn-deploy <service> --env prod --vtag cn-v1.2.3 # prod 发版
37
+ ```
38
+
39
+ - prod 是 vtag 制:必须先 `git tag cn-vX.Y.Z && git push`,工具拒绝无 vtag / 裸 v* / 带 --branch 的 prod 请求
40
+ - prod 流水线构建后**停在人工卡点**(xbfool/svenyang 在云效控制台审批),之后 DB 迁移 + digest 钉死部署自动走完
41
+ - 成功标准:prod SAE ImageUrl 为 `@sha256:` digest 寻址(工具最后一行校验)
42
+ - 用户请求发 cn-prod 时:确认已有 stage 验证过的 cn-v tag;没有则先引导走 stage
43
+
31
44
  ## 前置依赖
32
45
 
33
46
  - `aliyun` CLI 已配置(profile 默认 `aliyun-optima`,可用环境变量 `OPTIMA_ALIYUN_PROFILE` 覆盖为自己的 RAM 用户 profile)
@@ -35,5 +48,5 @@ optima-cn-deploy --list # 列出全部可发服务
35
48
 
36
49
  ## 边界
37
50
 
38
- - 只发 **cn-stage**,不发 cn-prod(cn-prod 发布另走人工卡点流程)。
51
+ - cn-prod 仅走 vtag 制(见上),且有人工卡点闸门;日常无 vtag 的构建只发 cn-stage。
39
52
  - 服务注册表是 optima-terraform `alicloud/stacks/cn-prod-buildbox/yunxiao/` 的快照;新增服务先在那边 gen-pipelines 建好流水线,再同步本工具的 SERVICES 表。
@@ -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
@@ -18,6 +18,9 @@
18
18
  * optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
19
19
  * optima-cn-deploy gateway-core --no-wait # 只触发不等待
20
20
  * optima-cn-deploy --list # 列出全部可发服务
21
+ * optima-cn-deploy billing --vtag cn-v1.2.3 # stage 按版本 tag 构建(发版前验证)
22
+ * optima-cn-deploy billing --env prod --vtag cn-v1.2.3
23
+ * # cn-prod vtag 发版:构建→停在人工卡点(云效控制台审批)→迁移→digest 钉死部署
21
24
  *
22
25
  * 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
23
26
  * 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
@@ -31,28 +34,28 @@ const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
31
34
  const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
32
35
 
33
36
  // 服务注册表(快照自 optima-terraform services.stage.env + gen-pipelines.py,2026-07-13)
34
- interface Svc { pipelineId: number; repo: string; saeAppId?: string; }
37
+ interface Svc { pipelineId: number; repo: string; saeAppId?: string; prodSaeAppId?: string; }
35
38
  const SERVICES: Record<string, Svc> = {
36
- 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
37
- 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
38
- 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
39
- 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
40
- 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
41
- 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
42
- 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
43
- 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
44
- 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
45
- 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
46
- 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
47
- 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
48
- 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
49
- 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
50
- 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
51
- 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
52
- 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
53
- 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
54
- 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
55
- 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
39
+ 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
40
+ 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
41
+ 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
42
+ 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
43
+ 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
44
+ 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
45
+ 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
46
+ 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
47
+ 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
48
+ 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
49
+ 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
50
+ 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
51
+ 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
52
+ 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
53
+ 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
54
+ 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
55
+ 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
56
+ 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
57
+ 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
58
+ 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
56
59
  };
57
60
 
58
61
  function sh(cmd: string, args: string[]): string {
@@ -88,10 +91,24 @@ async function main() {
88
91
  const bIdx = argv.indexOf('--branch');
89
92
  const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
90
93
  const noWait = argv.includes('--no-wait');
94
+ const eIdx = argv.indexOf('--env');
95
+ const envName = eIdx >= 0 ? argv[eIdx + 1] : 'stage';
96
+ const vIdx = argv.indexOf('--vtag');
97
+ const vtag = vIdx >= 0 ? argv[vIdx + 1] : '';
98
+ if (!['stage', 'prod'].includes(envName)) { console.error('✗ --env 只支持 stage|prod'); process.exit(1); }
99
+ if (vtag && !/^cn-v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(vtag)) {
100
+ console.error(`✗ vtag 格式必须 cn-vX.Y.Z(得到 ${vtag});裸 v* 会误触 AWS prod,绝不放行`); process.exit(1);
101
+ }
102
+ if (envName === 'prod') {
103
+ // prod = vtag 制:先 stage 同 tag 验证,再发 prod;流水线内还有人工卡点
104
+ if (!vtag) { console.error('✗ --env prod 必须带 --vtag cn-vX.Y.Z(先 stage 同 tag 验证)'); process.exit(1); }
105
+ if (branch !== 'main') { console.error('✗ prod 发布按 vtag,不接受 --branch'); process.exit(1); }
106
+ }
107
+ const ref = vtag || branch;
91
108
 
92
109
  // 1. GitHub HEAD + mirror 同步
93
- const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${branch}`, '--jq', '.sha']);
94
- if (!ghSha) { console.error(`✗ GitHub 上无 ${svc.repo}@${branch}(或 gh 未登录)`); process.exit(1); }
110
+ const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${ref}`, '--jq', '.sha']);
111
+ if (!ghSha) { console.error(`✗ GitHub 上无 ${svc.repo}@${ref}(或 gh 未登录,tag 需已 push)`); process.exit(1); }
95
112
  const repos = devops('ListRepositories', { perPage: '100' });
96
113
  const repoId = (repos.result || []).find((r: any) => r.name === svc.repo)?.Id;
97
114
  if (!repoId) { console.error(`✗ Codeup 无 mirror 仓 ${svc.repo}`); process.exit(1); }
@@ -99,29 +116,44 @@ async function main() {
99
116
  devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
100
117
  let synced = false;
101
118
  for (let i = 0; i < 30; i++) {
102
- const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: branch });
119
+ const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
103
120
  if (b?.result?.commit?.id === ghSha) { synced = true; break; }
104
121
  await sleep(10000);
105
122
  }
106
123
  if (!synced) { console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)'); process.exit(1); }
107
- console.log(`✓ mirror 已追平 ${svc.repo}@${branch} = ${ghSha.slice(0, 10)}`);
124
+ console.log(`✓ mirror 已追平 ${svc.repo}@${ref} = ${ghSha.slice(0, 10)}`);
108
125
 
109
- // 2. 触发(凭证由云效变量组 41970 供给,无需注入)
110
- const kv: Record<string, string> = { pipelineId: String(svc.pipelineId) };
111
- if (branch !== 'main') {
126
+ // 2. 触发(凭证由云效变量组供给,无需注入)。prod 流水线 id 按名实时解析(不硬编码)
127
+ let pipelineId = svc.pipelineId;
128
+ if (envName === 'prod') {
129
+ const lp = devops('ListPipelines', { maxResults: '100' });
130
+ const hit = (lp.pipelines || []).find((p: any) => p.pipelineName === `${svcName}-cn-prod`);
131
+ if (!hit) { console.error(`✗ 云效无 ${svcName}-cn-prod 流水线`); process.exit(1); }
132
+ pipelineId = hit.pipelineId;
133
+ }
134
+ const kv: Record<string, string> = { pipelineId: String(pipelineId) };
135
+ if (vtag) {
136
+ kv.params = JSON.stringify({
137
+ runningTags: { [`${CODEUP_BASE}/${svc.repo}.git`]: vtag },
138
+ envs: { VTAG: vtag },
139
+ });
140
+ } else if (branch !== 'main') {
112
141
  kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
113
142
  }
114
143
  const start = devops('StartPipelineRun', kv);
115
144
  const runId = start.pipelineRunId;
116
145
  if (!runId) { console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`); process.exit(1); }
117
- console.log(`▶ ${svcName} run#${runId} 已启动 (branch=${branch})`);
118
- console.log(` https://flow.aliyun.com/pipelines/${svc.pipelineId}/current`);
146
+ console.log(`▶ ${svcName} run#${runId} 已启动 (ref=${ref}, env=${envName})`);
147
+ console.log(` https://flow.aliyun.com/pipelines/${pipelineId}/current`);
148
+ if (envName === 'prod') {
149
+ console.log('⏸ 构建完成后会停在『发布审批』人工卡点 —— 需 xbfool/svenyang 在云效控制台通过,之后迁移+digest 部署自动走完');
150
+ }
119
151
  if (noWait) return;
120
152
 
121
153
  // 3. 轮询
122
154
  let status = '';
123
155
  for (;;) {
124
- const d = devops('GetPipelineRun', { pipelineId: String(svc.pipelineId), pipelineRunId: String(runId) });
156
+ const d = devops('GetPipelineRun', { pipelineId: String(pipelineId), pipelineRunId: String(runId) });
125
157
  status = d?.pipelineRun?.status || status;
126
158
  if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status)) break;
127
159
  await sleep(45000);
@@ -129,19 +161,26 @@ async function main() {
129
161
  console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
130
162
  if (status !== 'SUCCESS') process.exit(1);
131
163
 
132
- // 4. SAE ImageUrl 校验(空返回重试守卫)
133
- if (svc.saeAppId) {
164
+ // 4. SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
165
+ const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
166
+ if (appId) {
134
167
  let img = '';
135
168
  for (let i = 0; i < 3; i++) {
136
169
  const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
137
- 'DescribeApplicationConfig', '--AppId', svc.saeAppId]);
170
+ 'DescribeApplicationConfig', '--AppId', appId]);
138
171
  try { img = JSON.parse(raw)?.Data?.ImageUrl || ''; break; } catch { await sleep(3000); }
139
172
  }
140
173
  if (!img) { console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)'); return; }
141
- const tag = img.split(':').pop() || '';
142
- const ok = ghSha.startsWith(tag);
143
- console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
144
- if (!ok) process.exit(1);
174
+ if (envName === 'prod') {
175
+ const ok = img.includes('@sha256:');
176
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl(prod 应为 digest 寻址): ${img.split('/').pop()}`);
177
+ if (!ok) process.exit(1);
178
+ } else {
179
+ const tag = img.split(':').pop() || '';
180
+ const ok = ghSha.startsWith(tag);
181
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
182
+ if (!ok) process.exit(1);
183
+ }
145
184
  }
146
185
  }
147
186
 
@@ -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
+ }
@@ -20,6 +20,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  * optima-cn-deploy user-auth --branch feat/xxx # 非 main 分支构建+部署到 cn-stage
21
21
  * optima-cn-deploy gateway-core --no-wait # 只触发不等待
22
22
  * optima-cn-deploy --list # 列出全部可发服务
23
+ * optima-cn-deploy billing --vtag cn-v1.2.3 # stage 按版本 tag 构建(发版前验证)
24
+ * optima-cn-deploy billing --env prod --vtag cn-v1.2.3
25
+ * # cn-prod vtag 发版:构建→停在人工卡点(云效控制台审批)→迁移→digest 钉死部署
23
26
  *
24
27
  * 前置: aliyun CLI(profile 默认 aliyun-optima,可用 OPTIMA_ALIYUN_PROFILE 覆盖)+ gh 已登录。
25
28
  * 流水线定义的单一信源在 optima-terraform yunxiao/(gen-pipelines.py);本表为其快照,
@@ -31,26 +34,26 @@ const ENDPOINT = 'devops.cn-hangzhou.aliyuncs.com';
31
34
  const PROFILE = process.env.OPTIMA_ALIYUN_PROFILE || 'aliyun-optima';
32
35
  const CODEUP_BASE = `https://codeup.aliyun.com/${ORG}`;
33
36
  const SERVICES = {
34
- 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055' },
35
- 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37' },
36
- 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921' },
37
- 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb' },
38
- 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72' },
39
- 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68' },
40
- 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2' },
41
- 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108' },
42
- 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859' },
43
- 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d' },
44
- 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c' },
45
- 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa' },
46
- 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60' },
47
- 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807' },
48
- 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9' },
49
- 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5' },
50
- 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921' },
51
- 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2' },
52
- 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb' },
53
- 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6' },
37
+ 'agent-portal': { pipelineId: 5118519, repo: 'optima-portals', saeAppId: 'fe757f78-d18d-4480-9402-fe59d4721055', prodSaeAppId: '9211db38-a255-4393-ae92-7a7bb41b583d' },
38
+ 'agentic-chat': { pipelineId: 5118520, repo: 'agentic-chat', saeAppId: '6aea1ce1-f813-4e1c-8e97-d1ecb5398e37', prodSaeAppId: '6e290c73-a646-43ef-9da5-ad0b2e7eff73' },
39
+ 'billing': { pipelineId: 5118521, repo: 'optima-billing', saeAppId: '09d8e292-dc64-4af8-bce5-0a56cb666921', prodSaeAppId: '6c31cf82-8802-4d45-b6a2-e9d7c83ccce9' },
40
+ 'browser-backend': { pipelineId: 5118522, repo: 'optima-browser-use', saeAppId: '1fced3f6-a80a-41a4-8f23-e4d5a467f8eb', prodSaeAppId: 'eb782ed9-c468-4e81-a0fe-87e5b4264192' },
41
+ 'commerce-backend': { pipelineId: 5107005, repo: 'commerce-backend', saeAppId: '49d09808-508c-471a-9560-553c49a67f72', prodSaeAppId: 'd40597b6-a98c-4063-9297-8b9fdfa8add4' },
42
+ 'commerce-rq-scheduler': { pipelineId: 5118523, repo: 'commerce-backend', saeAppId: '1d2810ef-889e-4675-b6e6-a299e4722e68', prodSaeAppId: '762dd7a5-7fe8-41ed-9332-cfd31b0e087e' },
43
+ 'commerce-rq-worker': { pipelineId: 5118524, repo: 'commerce-backend', saeAppId: 'd661da87-6d24-40b8-93ed-e1c967d8abe2', prodSaeAppId: '2c9ee19a-5062-4205-9f5b-6ae3f8062a23' },
44
+ 'gateway-core': { pipelineId: 5118525, repo: 'optima-gateway', saeAppId: '9326b7ff-da52-48d6-86db-9c4a884be108', prodSaeAppId: 'a08ce23f-3d3e-4d89-a2cf-53c8adba614e' },
45
+ 'gw-admin': { pipelineId: 5118526, repo: 'optima-gateway', saeAppId: '90e0daf7-7910-46b8-b5bf-a0b8bcc60859', prodSaeAppId: 'c6bc5a78-b27f-46e2-825a-eaa338c23645' },
46
+ 'kb-backend': { pipelineId: 5118527, repo: 'kb-skills', saeAppId: 'c7f65160-9d9e-416e-9e36-5439010d2b2d', prodSaeAppId: '732dfc8c-ed78-43f7-a722-c614359ff1a1' },
47
+ 'ops-portal': { pipelineId: 5118529, repo: 'optima-portals', saeAppId: '41d0ee66-8402-4e72-bfda-8eba75d1270c', prodSaeAppId: '69e513fd-29bf-4355-a275-5eba82b21136' },
48
+ 'optima-generation': { pipelineId: 5118530, repo: 'optima-gen', saeAppId: '327856d5-8b18-4e15-bfd8-b8bd3b807ffa', prodSaeAppId: '868d1a69-12c9-4bb7-868b-3e89c26784b5' },
49
+ 'optima-generation-worker': { pipelineId: 5118531, repo: 'optima-gen', saeAppId: 'ad75b3b0-9ff4-443d-b0ef-81886bc7aa60', prodSaeAppId: '2adc941e-374b-406a-9863-e53d40bc9500' },
50
+ 'optima-scout': { pipelineId: 5117336, repo: 'optima-scout', saeAppId: 'bac2c3a4-90ce-49a6-81d5-caa38cb5c807', prodSaeAppId: 'f5bb7e82-e57c-4bd4-83ae-2c25f8d38647' },
51
+ 'optima-sentinel': { pipelineId: 5118532, repo: 'optima-sentinel', saeAppId: '3759db7b-4640-4a04-8436-4f241f0ec9d9', prodSaeAppId: 'e120e70c-b2c1-49ef-a66c-9016fd2d807d' },
52
+ 'optima-sentinel-worker': { pipelineId: 5118533, repo: 'optima-sentinel', saeAppId: '7c975caf-ebfb-4bb3-91c4-2a8c83d1d2e5', prodSaeAppId: 'e0f9b92c-00af-4ade-9b07-15bba48c14cc' },
53
+ 'optima-skills': { pipelineId: 5118534, repo: 'optima-skills', saeAppId: '90457be3-5eb3-4efc-a362-1788dcc51921', prodSaeAppId: '55a63ee7-fb75-46e4-99f3-20854a699237' },
54
+ 'user-auth': { pipelineId: 5118510, repo: 'user-auth', saeAppId: '36efc3a6-5b42-49fa-8db2-0865aa0c25d2', prodSaeAppId: 'd6fbf9de-fed6-4165-978f-7b3a0a456acc' },
55
+ 'user-auth-admin': { pipelineId: 5118535, repo: 'user-auth', saeAppId: '7feb9cc3-4731-4ea8-964c-4870a9e63afb', prodSaeAppId: '54093f99-37bf-4354-abd7-f6e76abdbcb6' },
56
+ 'yzsgo-api': { pipelineId: 5118536, repo: 'yzsgo', saeAppId: 'd0af7f66-ef66-4587-9e93-250d01ee3cf6', prodSaeAppId: '5a820992-e11b-404a-9e58-8f607d8e0c5f' },
54
57
  };
55
58
  function sh(cmd, args) {
56
59
  try {
@@ -90,10 +93,34 @@ async function main() {
90
93
  const bIdx = argv.indexOf('--branch');
91
94
  const branch = bIdx >= 0 ? argv[bIdx + 1] : 'main';
92
95
  const noWait = argv.includes('--no-wait');
96
+ const eIdx = argv.indexOf('--env');
97
+ const envName = eIdx >= 0 ? argv[eIdx + 1] : 'stage';
98
+ const vIdx = argv.indexOf('--vtag');
99
+ const vtag = vIdx >= 0 ? argv[vIdx + 1] : '';
100
+ if (!['stage', 'prod'].includes(envName)) {
101
+ console.error('✗ --env 只支持 stage|prod');
102
+ process.exit(1);
103
+ }
104
+ if (vtag && !/^cn-v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(vtag)) {
105
+ console.error(`✗ vtag 格式必须 cn-vX.Y.Z(得到 ${vtag});裸 v* 会误触 AWS prod,绝不放行`);
106
+ process.exit(1);
107
+ }
108
+ if (envName === 'prod') {
109
+ // prod = vtag 制:先 stage 同 tag 验证,再发 prod;流水线内还有人工卡点
110
+ if (!vtag) {
111
+ console.error('✗ --env prod 必须带 --vtag cn-vX.Y.Z(先 stage 同 tag 验证)');
112
+ process.exit(1);
113
+ }
114
+ if (branch !== 'main') {
115
+ console.error('✗ prod 发布按 vtag,不接受 --branch');
116
+ process.exit(1);
117
+ }
118
+ }
119
+ const ref = vtag || branch;
93
120
  // 1. GitHub HEAD + mirror 同步
94
- const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${branch}`, '--jq', '.sha']);
121
+ const ghSha = sh('gh', ['api', `repos/Optima-Chat/${svc.repo}/commits/${ref}`, '--jq', '.sha']);
95
122
  if (!ghSha) {
96
- console.error(`✗ GitHub 上无 ${svc.repo}@${branch}(或 gh 未登录)`);
123
+ console.error(`✗ GitHub 上无 ${svc.repo}@${ref}(或 gh 未登录,tag 需已 push)`);
97
124
  process.exit(1);
98
125
  }
99
126
  const repos = devops('ListRepositories', { perPage: '100' });
@@ -106,7 +133,7 @@ async function main() {
106
133
  devops('TriggerRepositoryMirrorSync', { repositoryId: String(repoId), account: 'xbfool', token: ghTok });
107
134
  let synced = false;
108
135
  for (let i = 0; i < 30; i++) {
109
- const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: branch });
136
+ const b = devops('GetBranchInfo', { repositoryId: String(repoId), branchName: ref });
110
137
  if (b?.result?.commit?.id === ghSha) {
111
138
  synced = true;
112
139
  break;
@@ -117,10 +144,26 @@ async function main() {
117
144
  console.error('✗ 300s 内 Codeup mirror 未追平 GitHub,中止(检查 mirror 凭证)');
118
145
  process.exit(1);
119
146
  }
120
- console.log(`✓ mirror 已追平 ${svc.repo}@${branch} = ${ghSha.slice(0, 10)}`);
121
- // 2. 触发(凭证由云效变量组 41970 供给,无需注入)
122
- const kv = { pipelineId: String(svc.pipelineId) };
123
- if (branch !== 'main') {
147
+ console.log(`✓ mirror 已追平 ${svc.repo}@${ref} = ${ghSha.slice(0, 10)}`);
148
+ // 2. 触发(凭证由云效变量组供给,无需注入)。prod 流水线 id 按名实时解析(不硬编码)
149
+ let pipelineId = svc.pipelineId;
150
+ if (envName === 'prod') {
151
+ const lp = devops('ListPipelines', { maxResults: '100' });
152
+ const hit = (lp.pipelines || []).find((p) => p.pipelineName === `${svcName}-cn-prod`);
153
+ if (!hit) {
154
+ console.error(`✗ 云效无 ${svcName}-cn-prod 流水线`);
155
+ process.exit(1);
156
+ }
157
+ pipelineId = hit.pipelineId;
158
+ }
159
+ const kv = { pipelineId: String(pipelineId) };
160
+ if (vtag) {
161
+ kv.params = JSON.stringify({
162
+ runningTags: { [`${CODEUP_BASE}/${svc.repo}.git`]: vtag },
163
+ envs: { VTAG: vtag },
164
+ });
165
+ }
166
+ else if (branch !== 'main') {
124
167
  kv.params = JSON.stringify({ runningBranchs: { [`${CODEUP_BASE}/${svc.repo}.git`]: branch } });
125
168
  }
126
169
  const start = devops('StartPipelineRun', kv);
@@ -129,14 +172,17 @@ async function main() {
129
172
  console.error(`✗ 启动失败: ${JSON.stringify(start).slice(0, 150)}`);
130
173
  process.exit(1);
131
174
  }
132
- console.log(`▶ ${svcName} run#${runId} 已启动 (branch=${branch})`);
133
- console.log(` https://flow.aliyun.com/pipelines/${svc.pipelineId}/current`);
175
+ console.log(`▶ ${svcName} run#${runId} 已启动 (ref=${ref}, env=${envName})`);
176
+ console.log(` https://flow.aliyun.com/pipelines/${pipelineId}/current`);
177
+ if (envName === 'prod') {
178
+ console.log('⏸ 构建完成后会停在『发布审批』人工卡点 —— 需 xbfool/svenyang 在云效控制台通过,之后迁移+digest 部署自动走完');
179
+ }
134
180
  if (noWait)
135
181
  return;
136
182
  // 3. 轮询
137
183
  let status = '';
138
184
  for (;;) {
139
- const d = devops('GetPipelineRun', { pipelineId: String(svc.pipelineId), pipelineRunId: String(runId) });
185
+ const d = devops('GetPipelineRun', { pipelineId: String(pipelineId), pipelineRunId: String(runId) });
140
186
  status = d?.pipelineRun?.status || status;
141
187
  if (['SUCCESS', 'FAIL', 'CANCELED', 'ERROR'].includes(status))
142
188
  break;
@@ -145,12 +191,13 @@ async function main() {
145
191
  console.log(`${status === 'SUCCESS' ? '✅' : '❌'} ${svcName} run#${runId}: ${status}`);
146
192
  if (status !== 'SUCCESS')
147
193
  process.exit(1);
148
- // 4. SAE ImageUrl 校验(空返回重试守卫)
149
- if (svc.saeAppId) {
194
+ // 4. SAE ImageUrl 校验(空返回重试守卫)。prod 是 digest 寻址,校验含 @sha256 即部署成功。
195
+ const appId = envName === 'prod' ? svc.prodSaeAppId : svc.saeAppId;
196
+ if (appId) {
150
197
  let img = '';
151
198
  for (let i = 0; i < 3; i++) {
152
199
  const raw = sh('aliyun', ['sae', '--profile', PROFILE, '--region', 'cn-beijing',
153
- 'DescribeApplicationConfig', '--AppId', svc.saeAppId]);
200
+ 'DescribeApplicationConfig', '--AppId', appId]);
154
201
  try {
155
202
  img = JSON.parse(raw)?.Data?.ImageUrl || '';
156
203
  break;
@@ -163,11 +210,19 @@ async function main() {
163
210
  console.log('⚠️ SAE 配置查询连续空返回,跳过 ImageUrl 校验(流水线本身已 SUCCESS)');
164
211
  return;
165
212
  }
166
- const tag = img.split(':').pop() || '';
167
- const ok = ghSha.startsWith(tag);
168
- console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
169
- if (!ok)
170
- process.exit(1);
213
+ if (envName === 'prod') {
214
+ const ok = img.includes('@sha256:');
215
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl(prod 应为 digest 寻址): ${img.split('/').pop()}`);
216
+ if (!ok)
217
+ process.exit(1);
218
+ }
219
+ else {
220
+ const tag = img.split(':').pop() || '';
221
+ const ok = ghSha.startsWith(tag);
222
+ console.log(`${ok ? '✓' : '✗'} SAE ImageUrl tag=${tag} vs 目标 ${ghSha.slice(0, 10)}`);
223
+ if (!ok)
224
+ process.exit(1);
225
+ }
171
226
  }
172
227
  }
173
228
  main().catch(e => { console.error('✗', e?.message || e); process.exit(1); });
@@ -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.12.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
+ }