@optima-chat/dev-skills 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -14,7 +14,7 @@ Prefer the installed CLI tools over reimplementing long shell workflows:
14
14
  - `optima-product <create|update|add-channel|toggle-channel|show> [options]` — manage paid-plugin marketplace Products + Stripe channels (Wave 1.5 admin endpoints; stage default)
15
15
  - `optima-entitlement <grant|revoke|list> [options]` — admin-grant / revoke / list paid-plugin entitlements (refuses revoke of PAYMENT / PARTNER source)
16
16
  - `optima-cn-deploy <service> [--branch feat/xxx] [--no-wait]` — 云效 Flow 发布到 cn-stage(mirror 同步→构建→DB 迁移→SAE 发布→sha 校验;20 服务,凭证由云效变量组供给零配置)
17
- - `optima-plugin <show|set-paid|set-default> [options]` — flip a plugin's skills-side paid/free state (isPaid) + defaultForUser (the user-facing gate; pairs with optima-product for the billing side)
17
+ - `optima-plugin <show|set-paid|set-default|set-status> [options]` — flip a plugin's skills-side paid/free state (isPaid) + defaultForUser (the user-facing gate; pairs with optima-product for the billing side) + lifecycle status (ACTIVE|BETA|DEPRECATED — retire/restore a marketplace plugin)
18
18
 
19
19
  For code-reading tasks across Optima repositories, use `gh` commands against `Optima-Chat/<repo>`.
20
20
 
@@ -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
@@ -0,0 +1,90 @@
1
+ import { callSkills, validateEnvCnProd } from '../billing-http';
2
+ import { confirmIfProd } from '../confirm-prompt';
3
+
4
+ export const VALID_PLUGIN_STATUSES = ['ACTIVE', 'BETA', 'DEPRECATED'] as const;
5
+ export type PluginStatus = (typeof VALID_PLUGIN_STATUSES)[number];
6
+
7
+ // Aligned with agent-runtime skill-sync-handler's isValidSlug (and the
8
+ // deprecate-plugin workflow guard in optima-default-skills#40).
9
+ const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
10
+
11
+ export interface SetStatusArgs {
12
+ slug: string;
13
+ status: PluginStatus;
14
+ yes: boolean;
15
+ env: string;
16
+ }
17
+
18
+ export function parseSetStatusArgs(argv: string[]): SetStatusArgs {
19
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
20
+ console.log(`Usage: optima-plugin set-status --slug <slug> --status ACTIVE|BETA|DEPRECATED [options]
21
+
22
+ Required:
23
+ --slug <slug>
24
+ --status <status> Sets Plugin.status (marketplace lifecycle gate).
25
+ DEPRECATED retires the plugin: registry sync stops
26
+ serving it and agents unload it on their next sync
27
+ (in-flight sessions keep it until then). ACTIVE
28
+ restores it. Fully reversible.
29
+
30
+ Optional:
31
+ --yes Skip prod confirmation prompt (no-op on stage)
32
+ --env <env> stage|prod|cn-prod|cn-stage (default: stage)
33
+
34
+ Note: this PATCH's 404 is the authoritative "slug has no marketplace row in
35
+ this env" signal. Don't use 'optima-plugin show' to check existence — it reads
36
+ the public endpoint, which also 404s for non-ACTIVE (e.g. already-DEPRECATED)
37
+ plugins.`);
38
+ process.exit(0);
39
+ }
40
+ const out: Partial<SetStatusArgs> = { env: 'stage', yes: false };
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ const next = argv[i + 1];
44
+ switch (a) {
45
+ case '--slug': out.slug = next; i++; break;
46
+ case '--status': {
47
+ const upper = (next ?? '').toUpperCase();
48
+ if (!(VALID_PLUGIN_STATUSES as readonly string[]).includes(upper)) {
49
+ throw new Error(`--status must be one of: ${VALID_PLUGIN_STATUSES.join('|')}`);
50
+ }
51
+ out.status = upper as PluginStatus; i++; break;
52
+ }
53
+ case '--yes': out.yes = true; break;
54
+ case '--env': out.env = next; i++; break;
55
+ default: throw new Error(`Unknown arg: ${a}`);
56
+ }
57
+ }
58
+ if (!out.slug) throw new Error('--slug required');
59
+ if (!SLUG_RE.test(out.slug)) throw new Error(`--slug must match ${SLUG_RE} (lowercase slug, e.g. onboarding-research)`);
60
+ if (!out.status) throw new Error(`--status required (${VALID_PLUGIN_STATUSES.join('|')})`);
61
+ return out as SetStatusArgs;
62
+ }
63
+
64
+ export async function runSetStatus(argv: string[]): Promise<void> {
65
+ const args = parseSetStatusArgs(argv);
66
+ validateEnvCnProd(args.env);
67
+
68
+ await confirmIfProd(
69
+ args.env,
70
+ `Action: set status=${args.status} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
71
+ args.yes,
72
+ );
73
+
74
+ console.log(`\n🚦 Setting status=${args.status} on ${args.slug} (${args.env.toUpperCase()})...`);
75
+ const res = await callSkills(
76
+ args.env,
77
+ 'PATCH',
78
+ `/api/admin/plugins/${encodeURIComponent(args.slug)}`,
79
+ { status: args.status },
80
+ );
81
+ console.log(`✓ Updated plugin (HTTP ${res.status}):`);
82
+ console.log(JSON.stringify(res.body, null, 2));
83
+ if (args.status === 'DEPRECATED') {
84
+ console.log(`\nℹ️ Takes effect on each user's next skill sync (new session / billing event): registry stops serving the plugin and agents unload its skills. In-flight sessions keep it until then. Restore anytime with --status ACTIVE. Verify retirement end-to-end by confirming an agent session no longer loads the plugin's skills.`);
85
+ } else if (args.status === 'ACTIVE') {
86
+ console.log(`\nℹ️ Plugin restored: registry serves it again on each user's next skill sync.`);
87
+ } else if (args.status === 'BETA') {
88
+ console.log(`\n⚠️ BETA is NOT served either: registry sync / system load / user install all filter status='ACTIVE' (optima-skills internal.ts & user-plugins.ts). Users lose the plugin on their next sync, same as DEPRECATED — it's a pre-GA gate, not a soft-launch channel.`);
89
+ }
90
+ }
@@ -3,6 +3,7 @@
3
3
  import { runShow } from './plugin/show';
4
4
  import { runSetPaid } from './plugin/set-paid';
5
5
  import { runSetDefault } from './plugin/set-default';
6
+ import { runSetStatus } from './plugin/set-status';
6
7
 
7
8
  function printHelp() {
8
9
  console.log(`Usage: optima-plugin <subcommand> [options]
@@ -11,6 +12,7 @@ Subcommands:
11
12
  show Show a plugin's marketplace state (isPaid, salesUrl, ... ACTIVE plugins only)
12
13
  set-paid Flip a plugin's isPaid flag (the user-facing paid/free gate)
13
14
  set-default Flip a plugin's defaultForUser flag
15
+ set-status Set a plugin's lifecycle status (ACTIVE|BETA|DEPRECATED — retire/restore)
14
16
 
15
17
  Run 'optima-plugin <subcommand> --help' for subcommand-specific options.`);
16
18
  }
@@ -22,6 +24,7 @@ async function main() {
22
24
  case 'show': await runShow(rest); break;
23
25
  case 'set-paid': await runSetPaid(rest); break;
24
26
  case 'set-default': await runSetDefault(rest); break;
27
+ case 'set-status': await runSetStatus(rest); break;
25
28
  default:
26
29
  console.error(`Unknown subcommand: ${subcommand}`);
27
30
  printHelp();
@@ -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
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VALID_PLUGIN_STATUSES = void 0;
4
+ exports.parseSetStatusArgs = parseSetStatusArgs;
5
+ exports.runSetStatus = runSetStatus;
6
+ const billing_http_1 = require("../billing-http");
7
+ const confirm_prompt_1 = require("../confirm-prompt");
8
+ exports.VALID_PLUGIN_STATUSES = ['ACTIVE', 'BETA', 'DEPRECATED'];
9
+ // Aligned with agent-runtime skill-sync-handler's isValidSlug (and the
10
+ // deprecate-plugin workflow guard in optima-default-skills#40).
11
+ const SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
12
+ function parseSetStatusArgs(argv) {
13
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
14
+ console.log(`Usage: optima-plugin set-status --slug <slug> --status ACTIVE|BETA|DEPRECATED [options]
15
+
16
+ Required:
17
+ --slug <slug>
18
+ --status <status> Sets Plugin.status (marketplace lifecycle gate).
19
+ DEPRECATED retires the plugin: registry sync stops
20
+ serving it and agents unload it on their next sync
21
+ (in-flight sessions keep it until then). ACTIVE
22
+ restores it. Fully reversible.
23
+
24
+ Optional:
25
+ --yes Skip prod confirmation prompt (no-op on stage)
26
+ --env <env> stage|prod|cn-prod|cn-stage (default: stage)
27
+
28
+ Note: this PATCH's 404 is the authoritative "slug has no marketplace row in
29
+ this env" signal. Don't use 'optima-plugin show' to check existence — it reads
30
+ the public endpoint, which also 404s for non-ACTIVE (e.g. already-DEPRECATED)
31
+ plugins.`);
32
+ process.exit(0);
33
+ }
34
+ const out = { env: 'stage', yes: false };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ const next = argv[i + 1];
38
+ switch (a) {
39
+ case '--slug':
40
+ out.slug = next;
41
+ i++;
42
+ break;
43
+ case '--status': {
44
+ const upper = (next ?? '').toUpperCase();
45
+ if (!exports.VALID_PLUGIN_STATUSES.includes(upper)) {
46
+ throw new Error(`--status must be one of: ${exports.VALID_PLUGIN_STATUSES.join('|')}`);
47
+ }
48
+ out.status = upper;
49
+ i++;
50
+ break;
51
+ }
52
+ case '--yes':
53
+ out.yes = true;
54
+ break;
55
+ case '--env':
56
+ out.env = next;
57
+ i++;
58
+ break;
59
+ default: throw new Error(`Unknown arg: ${a}`);
60
+ }
61
+ }
62
+ if (!out.slug)
63
+ throw new Error('--slug required');
64
+ if (!SLUG_RE.test(out.slug))
65
+ throw new Error(`--slug must match ${SLUG_RE} (lowercase slug, e.g. onboarding-research)`);
66
+ if (!out.status)
67
+ throw new Error(`--status required (${exports.VALID_PLUGIN_STATUSES.join('|')})`);
68
+ return out;
69
+ }
70
+ async function runSetStatus(argv) {
71
+ const args = parseSetStatusArgs(argv);
72
+ (0, billing_http_1.validateEnvCnProd)(args.env);
73
+ await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: set status=${args.status} on plugin '${args.slug}' (${args.env.toUpperCase()})`, args.yes);
74
+ console.log(`\n🚦 Setting status=${args.status} on ${args.slug} (${args.env.toUpperCase()})...`);
75
+ const res = await (0, billing_http_1.callSkills)(args.env, 'PATCH', `/api/admin/plugins/${encodeURIComponent(args.slug)}`, { status: args.status });
76
+ console.log(`✓ Updated plugin (HTTP ${res.status}):`);
77
+ console.log(JSON.stringify(res.body, null, 2));
78
+ if (args.status === 'DEPRECATED') {
79
+ console.log(`\nℹ️ Takes effect on each user's next skill sync (new session / billing event): registry stops serving the plugin and agents unload its skills. In-flight sessions keep it until then. Restore anytime with --status ACTIVE. Verify retirement end-to-end by confirming an agent session no longer loads the plugin's skills.`);
80
+ }
81
+ else if (args.status === 'ACTIVE') {
82
+ console.log(`\nℹ️ Plugin restored: registry serves it again on each user's next skill sync.`);
83
+ }
84
+ else if (args.status === 'BETA') {
85
+ console.log(`\n⚠️ BETA is NOT served either: registry sync / system load / user install all filter status='ACTIVE' (optima-skills internal.ts & user-plugins.ts). Users lose the plugin on their next sync, same as DEPRECATED — it's a pre-GA gate, not a soft-launch channel.`);
86
+ }
87
+ }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const show_1 = require("./plugin/show");
5
5
  const set_paid_1 = require("./plugin/set-paid");
6
6
  const set_default_1 = require("./plugin/set-default");
7
+ const set_status_1 = require("./plugin/set-status");
7
8
  function printHelp() {
8
9
  console.log(`Usage: optima-plugin <subcommand> [options]
9
10
 
@@ -11,6 +12,7 @@ Subcommands:
11
12
  show Show a plugin's marketplace state (isPaid, salesUrl, ... ACTIVE plugins only)
12
13
  set-paid Flip a plugin's isPaid flag (the user-facing paid/free gate)
13
14
  set-default Flip a plugin's defaultForUser flag
15
+ set-status Set a plugin's lifecycle status (ACTIVE|BETA|DEPRECATED — retire/restore)
14
16
 
15
17
  Run 'optima-plugin <subcommand> --help' for subcommand-specific options.`);
16
18
  }
@@ -30,6 +32,9 @@ async function main() {
30
32
  case 'set-default':
31
33
  await (0, set_default_1.runSetDefault)(rest);
32
34
  break;
35
+ case 'set-status':
36
+ await (0, set_status_1.runSetStatus)(rest);
37
+ break;
33
38
  default:
34
39
  console.error(`Unknown subcommand: ${subcommand}`);
35
40
  printHelp();
@@ -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.14.0",
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
+ }