@optima-chat/dev-skills 0.7.37 → 0.7.40

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.
@@ -1,23 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ // P15 D8b(optima-billing docs/2026-06-11-p15-wallet-sunset-spec.md):
5
+ // USD 钱包已退役——原「SSH 直写 usd_wallets.granted_balance_micros」作废,
6
+ // 改调 billing 服务态端点(grantCredits → bonus 积分)。
7
+ // ⚠️ 语义变化:旧 wallet granted 无期限;积分 bonus 桶标准 30 天有效期。
8
+ const crypto_1 = require("crypto");
4
9
  const db_utils_1 = require("./db-utils");
10
+ const billing_http_1 = require("./billing-http");
5
11
  function parseArgs(args) {
6
12
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
7
13
  console.log(`Usage: optima-grant-balance <email> --amount <usd> [options]
8
14
 
9
- Add USD balance to a user's wallet (granted_balance_micros).
15
+ Grant credits to a user (bonus bucket, expires in 30 days).
10
16
  Used for promotional grants, compensation, referral rewards, etc.
17
+ $1 = 700 credits (P15 unified ledger; the USD wallet is retired).
11
18
 
12
19
  Options:
13
- --amount <usd> USD amount to grant (required, e.g. 5 for $5.00)
20
+ --amount <usd> USD amount to grant (required, e.g. 5 for $5.00 = 3500 credits)
14
21
  --description <text> Description for audit trail (optional)
15
- --env <env> Environment: stage, prod (default: stage)
22
+ --env <env> Environment: stage, prod, cn-prod (default: stage)
16
23
  -h, --help Show this help
17
24
 
18
25
  Examples:
19
26
  optima-grant-balance user@example.com --amount 5 --env prod
20
- optima-grant-balance user@example.com --amount 10 --description "Service outage compensation"`);
27
+ optima-grant-balance user@example.com --amount 10 --description "Service outage compensation"
28
+ optima-grant-balance user@example.com --amount 1 --env cn-prod # ¥-priced env, still USD input ($1 = 700 credits)`);
21
29
  process.exit(0);
22
30
  }
23
31
  const email = args[0];
@@ -39,51 +47,35 @@ Examples:
39
47
  console.error('--amount is required and must be > 0 (USD)');
40
48
  process.exit(1);
41
49
  }
42
- if (!['stage', 'prod'].includes(env)) {
43
- console.error('Env must be stage or prod (billing DB not available in CI)');
44
- process.exit(1);
45
- }
50
+ (0, billing_http_1.validateEnvCnProd)(env);
46
51
  return { email, amountUsd, description, env };
47
52
  }
48
53
  async function main() {
49
54
  const { email, amountUsd, description, env } = parseArgs(process.argv.slice(2));
50
- const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
51
- const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
52
- // 1 USD = 1,000,000 micros. Round to integer micros.
53
- const amountMicros = Math.round(amountUsd * 1000000);
54
- console.log(`\n🎁 Granting $${amountUsd.toFixed(2)} to ${email} [${env.toUpperCase()}]\n`);
55
+ console.log(`\n🎁 Granting $${amountUsd.toFixed(2)} (${Math.round(amountUsd * 700)} credits) to ${email} [${env.toUpperCase()}]\n`);
55
56
  if (description)
56
57
  console.log(` Reason: ${description}`);
57
- const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
58
- const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
59
- const bq = billing.query;
60
- const now = new Date().toISOString();
61
- const safeUserId = (0, db_utils_1.escapeSQL)(userId);
62
- console.log(`Granting to wallet...`);
63
- const txSQL = `
64
- BEGIN;
65
-
66
- -- Ensure wallet exists
67
- INSERT INTO usd_wallets (id, user_id, balance_micros, reserved_micros, granted_balance_micros, created_at, updated_at)
68
- VALUES (gen_random_uuid(), '${safeUserId}', 0, 0, 0, '${now}', '${now}')
69
- ON CONFLICT (user_id) DO NOTHING;
70
-
71
- -- Add to granted balance
72
- UPDATE usd_wallets SET granted_balance_micros = granted_balance_micros + ${amountMicros}, updated_at = '${now}'
73
- WHERE user_id = '${safeUserId}';
74
-
75
- -- Record topup for audit trail (source='admin_grant' aligns with billing service convention)
76
- INSERT INTO usd_wallet_topups (id, wallet_id, amount_micros, service_fee_micros, net_credit_micros, status, source, service_namespace, created_at, completed_at)
77
- SELECT gen_random_uuid(), w.id, ${amountMicros}, 0, ${amountMicros}, 'completed', 'admin_grant', 'platform', '${now}', '${now}'
78
- FROM usd_wallets w WHERE w.user_id = '${safeUserId}';
79
-
80
- COMMIT;
81
- `.trim();
82
- bq(txSQL);
83
- console.log(`✓ Granted $${amountUsd.toFixed(2)} to wallet`);
84
- const balanceMicros = bq(`SELECT granted_balance_micros FROM usd_wallets WHERE user_id='${safeUserId}'`);
85
- const balanceUsd = (parseInt(balanceMicros, 10) / 1000000).toFixed(2);
86
- console.log(`\n✅ Done! ${email} now has $${balanceUsd} granted balance\n`);
58
+ // cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth's
59
+ // internal lookup API instead of the direct SQL path.
60
+ let userId;
61
+ if (env === 'cn-prod') {
62
+ userId = await (0, billing_http_1.resolveUserIdByEmail)(env, email);
63
+ }
64
+ else {
65
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
66
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
67
+ userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
68
+ }
69
+ // 幂等键 per-invocation 生成、callBilling 5xx retry 复用同 body —— 「已
70
+ // commit 但响应 5xx」场景重试不双发(billing spec R2-M3)。
71
+ const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-credits', {
72
+ userId,
73
+ amountUsd,
74
+ description: description ?? undefined,
75
+ idempotencyKey: `dev-skills-grant:${(0, crypto_1.randomUUID)()}`,
76
+ });
77
+ console.log(`✓ Granted ${body.credits} credits (lot ${body.lotId})`);
78
+ console.log(`\n✅ Done! ${email} received ${body.credits} bonus credits (expires in 30 days)\n`);
87
79
  }
88
80
  main().catch(error => {
89
81
  console.error('\n❌ Error:', error.message);
@@ -1,20 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ // P15 D8b:USD 钱包退役——原「SSH 直写 subscriptions/usd_wallets/token_quotas」
5
+ // 作废,改调 billing 服务态端点(与用户态 /api/admin/grant-subscription 同
6
+ // 业务体:supersede 旧授予 + 实得 credits + token quota,重试双发不双倍)。
4
7
  const db_utils_1 = require("./db-utils");
8
+ const billing_http_1 = require("./billing-http");
9
+ // cn-prod sells the CNY-priced -cn plans (P12); the bare USD plan ids also
10
+ // exist in the cn DB, so a per-env whitelist (not billing-side validation)
11
+ // is what prevents accidentally granting a USD-priced plan to a CN user.
12
+ const PLANS_BY_ENV = {
13
+ stage: ['trial', 'starter', 'pro', 'enterprise'],
14
+ prod: ['trial', 'starter', 'pro', 'enterprise'],
15
+ 'cn-prod': ['trial', 'starter-cn', 'pro-cn', 'enterprise-cn'],
16
+ };
5
17
  function parseArgs(args) {
6
18
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
7
19
  console.log(`Usage: optima-grant-subscription <email> [options]
8
20
 
9
21
  Options:
10
22
  --plan <id> Plan: trial, starter, pro, enterprise (default: pro)
23
+ cn-prod plans: trial, starter-cn, pro-cn, enterprise-cn (default: pro-cn)
11
24
  --months <n> Duration in months (default: 1)
12
- --env <env> Environment: stage, prod (default: stage)
25
+ --env <env> Environment: stage, prod, cn-prod (default: stage)
13
26
  -h, --help Show this help`);
14
27
  process.exit(0);
15
28
  }
16
29
  const email = args[0];
17
- let plan = 'pro';
30
+ let plan = null;
18
31
  let months = 1;
19
32
  let env = 'stage';
20
33
  for (let i = 1; i < args.length; i++) {
@@ -28,103 +41,40 @@ Options:
28
41
  env = args[++i];
29
42
  }
30
43
  }
31
- if (!['trial', 'starter', 'pro', 'enterprise'].includes(plan)) {
32
- console.error(`Unknown plan: ${plan}. Available: trial, starter, pro, enterprise`);
44
+ (0, billing_http_1.validateEnvCnProd)(env);
45
+ plan = plan ?? (env === 'cn-prod' ? 'pro-cn' : 'pro');
46
+ const allowed = PLANS_BY_ENV[env];
47
+ if (!allowed.includes(plan)) {
48
+ console.error(`Unknown plan for ${env}: ${plan}. Available: ${allowed.join(', ')}`);
33
49
  process.exit(1);
34
50
  }
35
51
  if (months < 1) {
36
52
  console.error('Months must be >= 1');
37
53
  process.exit(1);
38
54
  }
39
- if (!['stage', 'prod'].includes(env)) {
40
- console.error('Env must be stage or prod (billing DB not available in CI)');
41
- process.exit(1);
42
- }
43
55
  return { email, plan, months, env };
44
56
  }
45
57
  async function main() {
46
58
  const { email, plan, months, env } = parseArgs(process.argv.slice(2));
47
- const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
48
- const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
49
59
  console.log(`\n🎁 Granting ${plan} subscription to ${email} for ${months} month(s) [${env.toUpperCase()}]\n`);
50
- const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
51
- const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
52
- const bq = billing.query;
53
- // Read plan config from DB
54
- console.log(`Loading plan config: ${plan}`);
55
- const planRow = bq(`SELECT name, monthly_credits, session_token_limit, weekly_token_limit FROM plans WHERE id='${(0, db_utils_1.escapeSQL)(plan)}'`);
56
- if (!planRow) {
57
- console.error(`❌ Plan not found in DB: ${plan}`);
58
- process.exit(1);
60
+ // cn-prod has no SSH tunnel into the Aliyun RDS — resolve via user-auth's
61
+ // internal lookup API instead of the direct SQL path.
62
+ let userId;
63
+ if (env === 'cn-prod') {
64
+ userId = await (0, billing_http_1.resolveUserIdByEmail)(env, email);
59
65
  }
60
- const [planName, monthlyCreditsStr, sessionTokenLimitStr, weeklyTokenLimitStr] = planRow.split('|');
61
- const monthlyCredits = parseInt(monthlyCreditsStr, 10);
62
- const grantMicros = monthlyCredits * 10000; // 1 credit = $0.01 = 10,000 micros
63
- const sessionTokenLimit = parseInt(sessionTokenLimitStr, 10);
64
- const weeklyTokenLimit = parseInt(weeklyTokenLimitStr, 10);
65
- console.log(`✓ Plan: ${planName} (grant: $${(grantMicros / 1000000).toFixed(2)}, session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
66
- // Execute all mutations in a single transaction
67
- const now = new Date().toISOString();
68
- const periodEnd = new Date();
69
- periodEnd.setMonth(periodEnd.getMonth() + months);
70
- const periodEndISO = periodEnd.toISOString();
71
- const sessionEnd = new Date(new Date().getTime() + 5 * 60 * 60 * 1000).toISOString();
72
- const weekEnd = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
73
- const safeUserId = (0, db_utils_1.escapeSQL)(userId);
74
- const safePlan = (0, db_utils_1.escapeSQL)(plan);
75
- const safePlanName = (0, db_utils_1.escapeSQL)(planName);
76
- console.log('Executing transaction...');
77
- const txSQL = `
78
- BEGIN;
79
-
80
- -- Cancel active subscriptions
81
- UPDATE subscriptions SET status='canceled', canceled_at='${now}'
82
- WHERE user_id='${safeUserId}' AND status IN ('active','trialing');
83
-
84
- -- Create new subscription
85
- INSERT INTO subscriptions (id, user_id, plan_id, status, billing_interval, current_period_start, current_period_end, created_at, updated_at)
86
- VALUES (concat('sub_gift_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'active', 'monthly', '${now}', '${periodEndISO}', '${now}', '${now}');
87
-
88
- -- Ensure wallet exists (upsert)
89
- INSERT INTO usd_wallets (id, user_id, balance_micros, reserved_micros, granted_balance_micros, created_at, updated_at)
90
- VALUES (gen_random_uuid(), '${safeUserId}', 0, 0, 0, '${now}', '${now}')
91
- ON CONFLICT (user_id) DO NOTHING;
92
-
93
- -- Reset granted balance and set new grant
94
- UPDATE usd_wallets SET granted_balance_micros = ${grantMicros}, updated_at = '${now}'
95
- WHERE user_id = '${safeUserId}';
96
-
97
- -- Record topup for audit trail
98
- INSERT INTO usd_wallet_topups (id, wallet_id, amount_micros, service_fee_micros, net_credit_micros, status, source, service_namespace, created_at, completed_at)
99
- SELECT gen_random_uuid(), w.id, ${grantMicros}, 0, ${grantMicros}, 'completed', 'subscription_grant', 'platform', '${now}', '${now}'
100
- FROM usd_wallets w WHERE w.user_id = '${safeUserId}';
101
-
102
- -- Update existing active session quota, or insert new one if none exists
103
- UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${sessionTokenLimit}, updated_at='${now}'
104
- WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}';
105
-
106
- INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
107
- SELECT concat('tq_sess_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'session', ${sessionTokenLimit}, 0, '${now}', '${sessionEnd}', '${now}', '${now}'
108
- WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='session' AND period_end > '${now}');
109
-
110
- -- Update existing active weekly quota, or insert new one if none exists
111
- UPDATE token_quotas SET plan_id='${safePlan}', monthly_limit=${weeklyTokenLimit}, updated_at='${now}'
112
- WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}';
113
-
114
- INSERT INTO token_quotas (id, user_id, plan_id, period_type, monthly_limit, monthly_used, period_start, period_end, created_at, updated_at)
115
- SELECT concat('tq_week_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safePlan}', 'weekly', ${weeklyTokenLimit}, 0, '${now}', '${weekEnd}', '${now}', '${now}'
116
- WHERE NOT EXISTS (SELECT 1 FROM token_quotas WHERE user_id='${safeUserId}' AND period_type='weekly' AND period_end > '${now}');
117
-
118
- COMMIT;
119
- `.trim();
120
- bq(txSQL);
121
- console.log('✓ Old subscriptions canceled');
122
- console.log(`✓ ${planName} subscription created (expires: ${periodEnd.toLocaleDateString()})`);
123
- if (grantMicros > 0) {
124
- console.log(`✓ Wallet granted $${(grantMicros / 1000000).toFixed(2)} (${monthlyCredits} credits equivalent)`);
66
+ else {
67
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
68
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
69
+ userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
125
70
  }
126
- console.log(`✓ Token quotas updated (session: ${sessionTokenLimit.toLocaleString()}, weekly: ${weeklyTokenLimit.toLocaleString()})`);
127
- console.log(`\n✅ Done! ${email} now has ${planName} plan until ${periodEnd.toLocaleDateString()}\n`);
71
+ const { body } = await (0, billing_http_1.callBilling)(env, 'POST', '/api/billing/admin/grant-subscription', { userId, planId: plan, months });
72
+ console.log(`✓ Subscription ${body.subscriptionId} (${body.planId})`);
73
+ console.log(`✓ Credits: ${body.credits.toLocaleString()} (expires ${body.expiresAt})`);
74
+ console.log(`✓ Token limits: session ${body.sessionTokenLimit.toLocaleString()} / weekly ${body.weeklyTokenLimit.toLocaleString()}`);
75
+ if (body.warning)
76
+ console.log(`⚠️ ${body.warning}`);
77
+ console.log(`\n✅ Done! ${email} now has ${body.planId} until ${body.expiresAt}\n`);
128
78
  }
129
79
  main().catch(error => {
130
80
  console.error('\n❌ Error:', error.message);
File without changes
File without changes
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const child_process_1 = require("child_process");
38
38
  const fs = __importStar(require("fs"));
39
+ const db_utils_1 = require("./db-utils");
39
40
  const SERVICE_DB_MAP = {
40
41
  'commerce-backend': {
41
42
  ci: { container: 'commerce-postgres', user: 'commerce', password: 'commerce123', database: 'commerce' },
@@ -113,64 +114,8 @@ const RDS_HOSTS = {
113
114
  stage: 'optima-stage-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com',
114
115
  prod: 'optima-prod-postgres.ctg866o0ehac.ap-southeast-1.rds.amazonaws.com'
115
116
  };
116
- // 统一使用 BI Data ARM Host 作为跳板机
117
- const EC2_HOST = '3.0.210.113';
118
- function parseDatabaseUrl(url) {
119
- // postgresql://user:password@host:port/database?params
120
- const match = url.match(/^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
121
- if (!match) {
122
- throw new Error(`Failed to parse DATABASE_URL: ${url}`);
123
- }
124
- return {
125
- user: decodeURIComponent(match[1]),
126
- password: decodeURIComponent(match[2]),
127
- host: match[3],
128
- port: parseInt(match[4]),
129
- database: match[5]
130
- };
131
- }
132
- function getGitHubVariable(name) {
133
- return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
134
- }
135
- function getInfisicalConfig() {
136
- return {
137
- url: getGitHubVariable('INFISICAL_URL'),
138
- clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
139
- clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
140
- projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
141
- };
142
- }
143
- function getInfisicalToken(config) {
144
- const response = (0, child_process_1.execSync)(`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`, { encoding: 'utf-8' });
145
- return JSON.parse(response).accessToken;
146
- }
147
- function getInfisicalSecrets(config, token, environment, secretPath) {
148
- const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${secretPath}" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
149
- const data = JSON.parse(response);
150
- const secrets = {};
151
- for (const secret of data.secrets || []) {
152
- secrets[secret.secretKey] = secret.secretValue;
153
- }
154
- return secrets;
155
- }
156
- function setupSSHTunnel(ec2Host, dbHost, localPort) {
157
- // 检查是否已有隧道
158
- try {
159
- (0, child_process_1.execSync)(`lsof -ti:${localPort}`, { stdio: 'ignore' });
160
- console.log(`✓ SSH tunnel already exists on port ${localPort}`);
161
- return;
162
- }
163
- catch {
164
- // 端口未占用,创建隧道
165
- }
166
- const sshKeyPath = `${process.env.HOME}/.ssh/optima-ec2-key`;
167
- if (!fs.existsSync(sshKeyPath)) {
168
- throw new Error(`SSH key not found: ${sshKeyPath}. Please obtain optima-ec2-key from xbfool.`);
169
- }
170
- console.log(`Creating SSH tunnel: localhost:${localPort} -> ${ec2Host} -> ${dbHost}:5432`);
171
- (0, child_process_1.execSync)(`ssh -i ${sshKeyPath} -f -N -o StrictHostKeyChecking=no -L ${localPort}:${dbHost}:5432 ec2-user@${ec2Host}`, { stdio: 'inherit' });
172
- console.log(`✓ SSH tunnel established on port ${localPort}`);
173
- }
117
+ // parseDatabaseUrl 统一用 db-utils 的实现:右切 userinfo 容忍密码特殊字符,
118
+ // 且报错不回显 URL(这里曾把含密码的完整 URL 打进错误信息)。
174
119
  function findPsqlPath() {
175
120
  // 1. 优先从 PATH 中查找
176
121
  const whichCmd = process.platform === 'win32' ? 'where psql' : 'which psql';
@@ -225,7 +170,7 @@ async function main() {
225
170
  console.error('Usage: query-db.ts <service> <sql> [environment]');
226
171
  console.error('');
227
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');
228
- console.error('Environments: ci (default), stage, prod');
173
+ console.error('Environments: ci (default), stage, prod, cn (阿里云 cn-prod)');
229
174
  console.error('');
230
175
  console.error('Example: query-db.ts user-auth "SELECT COUNT(*) FROM users" prod');
231
176
  process.exit(1);
@@ -236,6 +181,27 @@ async function main() {
236
181
  console.error('Available services:', Object.keys(SERVICE_DB_MAP).join(', '));
237
182
  process.exit(1);
238
183
  }
184
+ // cn-prod(阿里云):独立 Infisical + 经 buildbox 跳板连内网 RDS(动态端口隧道)。
185
+ // 两类 cred:① shared-secrets/database-users(按 prefix)② 服务自己的 DATABASE_URL(展开引用)。
186
+ if ((0, db_utils_1.isCnEnv)(environment)) {
187
+ const prodCfg = SERVICE_DB_MAP[service].prod;
188
+ let db;
189
+ if (prodCfg?.userKey) {
190
+ const prefix = prodCfg.userKey.replace(/_DB_USER$/, '');
191
+ console.log(`\n🔍 Querying ${service} (CN-PROD, prefix ${prefix})...`);
192
+ db = (0, db_utils_1.connectCnDB)(prefix);
193
+ }
194
+ else if (prodCfg?.databaseUrlPath) {
195
+ console.log(`\n🔍 Querying ${service} (CN-PROD, DATABASE_URL @ ${prodCfg.databaseUrlPath})...`);
196
+ db = (0, db_utils_1.connectCnDBFromUrl)(prodCfg.databaseUrlPath);
197
+ }
198
+ else {
199
+ console.error(`cn-prod query 暂不支持 ${service}(既无 userKey 也无 databaseUrlPath)。见 optima-dev-skills#21。`);
200
+ process.exit(1);
201
+ }
202
+ console.log('\n' + db.query(sql));
203
+ return;
204
+ }
239
205
  const serviceConfig = SERVICE_DB_MAP[service][environment];
240
206
  if (!serviceConfig) {
241
207
  console.error(`Service ${service} is not available in ${environment.toUpperCase()} environment.`);
@@ -247,18 +213,18 @@ async function main() {
247
213
  console.log(`\n🔍 Querying ${service} (${environment.toUpperCase()})...`);
248
214
  if (environment === 'ci') {
249
215
  // CI 环境:通过 SSH + Docker Exec
250
- const ciUser = getGitHubVariable('CI_SSH_USER');
251
- const ciHost = getGitHubVariable('CI_SSH_HOST');
252
- const ciPassword = getGitHubVariable('CI_SSH_PASSWORD');
216
+ const ciUser = (0, db_utils_1.getGitHubVariable)('CI_SSH_USER');
217
+ const ciHost = (0, db_utils_1.getGitHubVariable)('CI_SSH_HOST');
218
+ const ciPassword = (0, db_utils_1.getGitHubVariable)('CI_SSH_PASSWORD');
253
219
  const { container, user, database } = serviceConfig;
254
220
  const result = (0, child_process_1.execSync)(`sshpass -p "${ciPassword}" ssh -o StrictHostKeyChecking=no ${ciUser}@${ciHost} "docker exec ${container} psql -U ${user} -d ${database} -c \\"${sql}\\""`, { encoding: 'utf-8' });
255
221
  console.log('\n' + result);
256
222
  }
257
223
  else {
258
224
  // Stage/Prod 环境:通过 SSH 隧道访问 RDS
259
- const infisicalConfig = getInfisicalConfig();
225
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
260
226
  console.log('✓ Loaded Infisical config from GitHub Variables');
261
- const token = getInfisicalToken(infisicalConfig);
227
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
262
228
  console.log('✓ Obtained Infisical access token');
263
229
  const infisicalEnv = environment === 'stage' ? 'staging' : 'prod';
264
230
  let dbUser;
@@ -268,13 +234,13 @@ async function main() {
268
234
  if ('databaseUrlPath' in serviceConfig) {
269
235
  // 从服务路径获取 DATABASE_URL 并解析
270
236
  const { databaseUrlPath, databaseUrlKey } = serviceConfig;
271
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, databaseUrlPath);
237
+ const secrets = (0, db_utils_1.getInfisicalSecrets)(infisicalConfig, token, infisicalEnv, databaseUrlPath);
272
238
  console.log(`✓ Retrieved DATABASE_URL from Infisical (path: ${databaseUrlPath})`);
273
239
  const databaseUrl = secrets[databaseUrlKey];
274
240
  if (!databaseUrl) {
275
241
  throw new Error(`DATABASE_URL not found in Infisical at ${databaseUrlPath}`);
276
242
  }
277
- const parsed = parseDatabaseUrl(databaseUrl);
243
+ const parsed = (0, db_utils_1.parseDatabaseUrl)(databaseUrl);
278
244
  dbUser = parsed.user;
279
245
  dbPassword = parsed.password;
280
246
  dbHost = parsed.host;
@@ -282,7 +248,7 @@ async function main() {
282
248
  }
283
249
  else {
284
250
  // 从 shared-secrets/database-users 获取凭证
285
- const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
251
+ const secrets = (0, db_utils_1.getInfisicalSecrets)(infisicalConfig, token, infisicalEnv, '/shared-secrets/database-users');
286
252
  console.log('✓ Retrieved database credentials from Infisical');
287
253
  const { userKey, passwordKey } = serviceConfig;
288
254
  database = serviceConfig.database;
@@ -293,10 +259,7 @@ async function main() {
293
259
  throw new Error(`Database credentials not found in Infisical for ${service}. Keys: ${userKey}, ${passwordKey}`);
294
260
  }
295
261
  }
296
- const localPort = environment === 'stage' ? 15432 : 15433;
297
- setupSSHTunnel(EC2_HOST, dbHost, localPort);
298
- // 等待隧道建立
299
- await new Promise(resolve => setTimeout(resolve, 1000));
262
+ const localPort = (0, db_utils_1.ensureTunnel)(dbHost);
300
263
  const result = queryDatabase('localhost', localPort, dbUser, dbPassword, database, sql);
301
264
  console.log('\n' + result);
302
265
  }
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const child_process_1 = require("child_process");
5
+ const db_utils_1 = require("./db-utils");
5
6
  // 支持的服务列表(Infisical 路径为 /services/<service-name>)
6
7
  const SUPPORTED_SERVICES = [
7
8
  'ads-backend',
@@ -35,21 +36,8 @@ const ENV_MAP = {
35
36
  stage: 'staging',
36
37
  prod: 'prod'
37
38
  };
38
- function getGitHubVariable(name) {
39
- return (0, child_process_1.execSync)(`gh variable get ${name} -R Optima-Chat/optima-dev-skills`, { encoding: 'utf-8' }).trim();
40
- }
41
- function getInfisicalConfig() {
42
- return {
43
- url: getGitHubVariable('INFISICAL_URL'),
44
- clientId: getGitHubVariable('INFISICAL_CLIENT_ID'),
45
- clientSecret: getGitHubVariable('INFISICAL_CLIENT_SECRET'),
46
- projectId: getGitHubVariable('INFISICAL_PROJECT_ID')
47
- };
48
- }
49
- function getInfisicalToken(config) {
50
- const response = (0, child_process_1.execSync)(`curl -s -X POST "${config.url}/api/v1/auth/universal-auth/login" -H "Content-Type: application/json" -d '{"clientId": "${config.clientId}", "clientSecret": "${config.clientSecret}"}'`, { encoding: 'utf-8' });
51
- return JSON.parse(response).accessToken;
52
- }
39
+ // NOTE: getInfisicalSecrets is kept local because it encodes secretPath (encodeURIComponent),
40
+ // unlike db-utils' raw-path variant; getGitHubVariable/getInfisicalConfig/getInfisicalToken are shared from db-utils.
53
41
  function getInfisicalSecrets(config, token, environment, secretPath) {
54
42
  const response = (0, child_process_1.execSync)(`curl -s "${config.url}/api/v3/secrets/raw?workspaceId=${config.projectId}&environment=${environment}&secretPath=${encodeURIComponent(secretPath)}" -H "Authorization: Bearer ${token}"`, { encoding: 'utf-8' });
55
43
  const data = JSON.parse(response);
@@ -122,9 +110,9 @@ async function main() {
122
110
  const secretPath = `/services/${service}`;
123
111
  console.log(`\n🔍 Fetching environment variables for ${service} (${environment.toUpperCase()})...\n`);
124
112
  try {
125
- const infisicalConfig = getInfisicalConfig();
113
+ const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
126
114
  console.log('✓ Loaded Infisical config from GitHub Variables');
127
- const token = getInfisicalToken(infisicalConfig);
115
+ const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
128
116
  console.log('✓ Obtained Infisical access token');
129
117
  const infisicalEnv = ENV_MAP[environment];
130
118
  const secrets = getInfisicalSecrets(infisicalConfig, token, infisicalEnv, secretPath);