@optima-chat/dev-skills 0.7.36 → 0.7.37

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.
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runList = runList;
4
+ const billing_http_1 = require("../billing-http");
5
+ function parseArgs(argv) {
6
+ if (argv[0] === '-h' || argv[0] === '--help') {
7
+ console.log(`Usage: optima-discount list [options]
8
+
9
+ Optional:
10
+ --campaign <label> Filter by campaign
11
+ --code <CODE> Filter by exact code
12
+ --limit <N> Max rows (default 500, max 1000)
13
+ --env stage|prod (default: stage)`);
14
+ process.exit(0);
15
+ }
16
+ const out = { env: 'stage' };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ const next = argv[i + 1];
20
+ switch (a) {
21
+ case '--campaign':
22
+ out.campaign = next;
23
+ i++;
24
+ break;
25
+ case '--code':
26
+ out.code = next;
27
+ i++;
28
+ break;
29
+ case '--limit': {
30
+ const n = parseInt(next, 10);
31
+ if (isNaN(n))
32
+ throw new Error('--limit requires a number');
33
+ out.limit = n;
34
+ i++;
35
+ break;
36
+ }
37
+ case '--env':
38
+ out.env = next;
39
+ i++;
40
+ break;
41
+ default: throw new Error(`Unknown arg: ${a}`);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ async function runList(argv) {
47
+ const args = parseArgs(argv);
48
+ (0, billing_http_1.validateEnv)(args.env);
49
+ const qs = new URLSearchParams();
50
+ if (args.campaign)
51
+ qs.set('campaign', args.campaign);
52
+ if (args.code)
53
+ qs.set('code', args.code);
54
+ if (args.limit !== undefined)
55
+ qs.set('limit', String(args.limit));
56
+ const suffix = qs.toString() ? `?${qs.toString()}` : '';
57
+ const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/discount-codes${suffix}`);
58
+ console.log(JSON.stringify(res.body, null, 2));
59
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const create_1 = require("./discount/create");
5
+ const generate_1 = require("./discount/generate");
6
+ const list_1 = require("./discount/list");
7
+ const disable_1 = require("./discount/disable");
8
+ function printHelp() {
9
+ console.log(`Usage: optima-discount <subcommand> [options]
10
+
11
+ Subcommands:
12
+ create Create one discount code (shared or single-use)
13
+ generate Generate N unique single-use codes (written to a file)
14
+ list List discount codes (filter by campaign/code)
15
+ disable Disable a discount code
16
+
17
+ Run 'optima-discount <subcommand> --help' for subcommand-specific options.`);
18
+ }
19
+ async function main() {
20
+ const [, , subcommand, ...rest] = process.argv;
21
+ if (!subcommand || subcommand === '-h' || subcommand === '--help') {
22
+ printHelp();
23
+ process.exit(0);
24
+ }
25
+ switch (subcommand) {
26
+ case 'create':
27
+ await (0, create_1.runCreate)(rest);
28
+ break;
29
+ case 'generate':
30
+ await (0, generate_1.runGenerate)(rest);
31
+ break;
32
+ case 'list':
33
+ await (0, list_1.runList)(rest);
34
+ break;
35
+ case 'disable':
36
+ await (0, disable_1.runDisable)(rest);
37
+ break;
38
+ default:
39
+ console.error(`Unknown subcommand: ${subcommand}`);
40
+ printHelp();
41
+ process.exit(1);
42
+ }
43
+ }
44
+ main().catch((err) => {
45
+ console.error(err.message);
46
+ process.exit(1);
47
+ });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,517 @@
1
+ # 优惠码 Plan B — optima-dev-skills `optima-discount` CLI
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development to execute task-by-task. Steps use `- [ ]`.
4
+
5
+ **Goal:** 在 optima-dev-skills 加一个 `optima-discount` thin HTTP CLI(create/generate/list/disable)+ SKILL.md,调 billing 的 `/api/billing/admin/discount-codes*` 端点(Plan A 已 merge 进 billing main),供运营发优惠码。
6
+
7
+ **Architecture:** 对标现有 `optima-product`:dispatcher (`bin/helpers/discount.ts`) + 每子命令一个文件 (`bin/helpers/discount/<cmd>.ts`),经 `callBilling(env, method, path, body)`(`billing-http.ts`,service-JWT/OAuth,dev-skills 客户端在 billing allow-list)。`callService` 对非 2xx **抛错**,dispatcher 的 `main().catch` 打印 `err.message` 退 1 —— 子命令保持 thin,不查 status。prod 写操作用 `confirmIfProd(env, desc, --yes)`。
8
+
9
+ **Tech Stack:** TypeScript(`tsc` → `dist/`,`bin/**/*` 被 tsconfig include),Node 内置(fs/process),零新依赖。
10
+
11
+ **Spec:** billing repo `docs/2026-05-27-discount-codes-design.md` §6 + §4.6。
12
+
13
+ **Branch / worktree:** `feat/discount-cli`(off `origin/main`)于 `optima-dev-skills/.worktrees/feat/discount-cli/`。**每个 bash 先 `cd` 到此 worktree + `pwd`**。PR base = `main`。
14
+
15
+ > **验证约定**:dev-skills **无单测框架**(package.json 仅 build/prepare/postinstall;现有 CLI 如 product/grant-balance 均无单测)。本 plan 不引入测试框架(避免 scope creep)——验证 = **`npm run build`(tsc 0 error,type-check `bin/**/*`)**。真正的端到端(调 stage billing admin 端点发码)是**部署后的手动/CI 验证**,见收尾,本地不可做(需 billing 部署到 stage + dev-skills service client)。
16
+
17
+ ---
18
+
19
+ ## File Structure
20
+
21
+ **Create:**
22
+ - `bin/helpers/discount.ts` — dispatcher(import 4 个 run* + switch)
23
+ - `bin/helpers/discount/create.ts` — `runCreate`:建单个码
24
+ - `bin/helpers/discount/generate.ts` — `runGenerate`:批量唯一码,码写文件
25
+ - `bin/helpers/discount/list.ts` — `runList`:列出
26
+ - `bin/helpers/discount/disable.ts` — `runDisable`:停用
27
+ - `.claude/skills/discount-codes/SKILL.md` — 技能说明
28
+
29
+ **Modify:**
30
+ - `package.json` — `bin` 加 `"optima-discount": "dist/bin/helpers/discount.js"`
31
+ - `README.md` — skill 列表 + CLI 工具表加 discount
32
+ - `.gitignore` — 已加 `.worktrees/`(首个 commit 带上)
33
+
34
+ > ⚠️ **核对契约看 `origin/main`,不是 billing 本地工作树**:billing 本地 checkout 在 `integration/wave-1.5-deploy`,磁盘上没有 discount 文件;端点在 `origin/main`(billing#65 已 merge)。要核对用 `git show origin/main:src/routes/admin-discount-codes.ts`。
35
+ > **越界值服务端兜底**(thin client 哲学):`--percent` 越界(非 1..100)、`--count` 越界(非 1..1000)、`--max 0` 由 billing 服务端校验返 400,`callService` 抛错 → dispatcher 打印干净错误。CLI 只做 `isNaN` 这类基本检查,不重复服务端的业务边界校验。
36
+
37
+ **端点契约**(billing Plan A 已实现,service-JWT 守卫 `requireAdminService`):
38
+ - `POST /api/billing/admin/discount-codes` body `{code, percentOff, productKeys?, startsAt?, endsAt?, maxRedemptions?, campaign?}` → 201 创建的行
39
+ - `POST /api/billing/admin/discount-codes/batch` body `{count, percentOff, campaign, productKeys?, startsAt?, endsAt?}` → 201 `{codes: string[]}`
40
+ - `GET /api/billing/admin/discount-codes?campaign=&code=&limit=` → `{codes: [...]}`
41
+ - `PATCH /api/billing/admin/discount-codes/:code` body `{status:"DISABLED"}` → 200 行
42
+ (日期字段 billing 用 `z.iso.datetime()` —— CLI 把 `--starts/--ends` 用 `new Date(v).toISOString()` 归一化成完整 ISO datetime 再发,支持用户传 `2026-06-30` 或完整 datetime。)
43
+
44
+ ---
45
+
46
+ ## Task 1: 4 个子命令 + dispatcher + bin 注册
47
+
48
+ **Files:** Create `bin/helpers/discount/{create,generate,list,disable}.ts` + `bin/helpers/discount.ts`; Modify `package.json`.
49
+
50
+ - [ ] **Step 1: `bin/helpers/discount/create.ts`**
51
+
52
+ ```ts
53
+ import { callBilling, validateEnv } from '../billing-http';
54
+ import { confirmIfProd } from '../confirm-prompt';
55
+
56
+ interface CreateArgs {
57
+ code: string;
58
+ percentOff: number;
59
+ productKeys?: string[];
60
+ startsAt?: string;
61
+ endsAt?: string;
62
+ maxRedemptions?: number;
63
+ campaign?: string;
64
+ env: string;
65
+ yes: boolean;
66
+ }
67
+
68
+ /** Normalize a date/datetime arg to a full ISO datetime string (billing requires z.iso.datetime). */
69
+ function toIso(v: string): string {
70
+ const d = new Date(v);
71
+ if (isNaN(d.getTime())) throw new Error(`Invalid date: ${v} (use YYYY-MM-DD or ISO datetime)`);
72
+ return d.toISOString();
73
+ }
74
+
75
+ function parseArgs(argv: string[]): CreateArgs {
76
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
77
+ console.log(`Usage: optima-discount create --code <CODE> --percent <1-100> [options]
78
+
79
+ Required:
80
+ --code <CODE> Promo code (stored uppercased)
81
+ --percent <1-100> Percentage off
82
+
83
+ Optional:
84
+ --products <a,b,...> Limit to these productKeys (default: all)
85
+ --starts <date> Valid-from (YYYY-MM-DD or ISO; default: immediately)
86
+ --ends <date> Valid-until
87
+ --max <N> Max total redemptions (default: unlimited; 1 = single-use)
88
+ --campaign <label> Grouping label
89
+ --env stage|prod (default: stage)
90
+ --yes Skip prod confirmation`);
91
+ process.exit(0);
92
+ }
93
+ const out: Partial<CreateArgs> = { env: 'stage', yes: false };
94
+ for (let i = 0; i < argv.length; i++) {
95
+ const a = argv[i];
96
+ const next = argv[i + 1];
97
+ switch (a) {
98
+ case '--code': out.code = next; i++; break;
99
+ case '--percent': out.percentOff = parseInt(next, 10); i++; break;
100
+ case '--products': out.productKeys = next.split(',').map((s) => s.trim()).filter(Boolean); i++; break;
101
+ case '--starts': out.startsAt = toIso(next); i++; break;
102
+ case '--ends': out.endsAt = toIso(next); i++; break;
103
+ case '--max': out.maxRedemptions = parseInt(next, 10); i++; break;
104
+ case '--campaign': out.campaign = next; i++; break;
105
+ case '--env': out.env = next; i++; break;
106
+ case '--yes': out.yes = true; break;
107
+ default: throw new Error(`Unknown arg: ${a}`);
108
+ }
109
+ }
110
+ if (!out.code) throw new Error('--code required');
111
+ if (out.percentOff === undefined || isNaN(out.percentOff)) throw new Error('--percent required (1-100)');
112
+ return out as CreateArgs;
113
+ }
114
+
115
+ export async function runCreate(argv: string[]): Promise<void> {
116
+ const args = parseArgs(argv);
117
+ validateEnv(args.env);
118
+ await confirmIfProd(args.env, `Create discount code ${args.code.toUpperCase()} (${args.percentOff}% off)`, args.yes);
119
+
120
+ const body: Record<string, unknown> = { code: args.code, percentOff: args.percentOff };
121
+ if (args.productKeys) body.productKeys = args.productKeys;
122
+ if (args.startsAt) body.startsAt = args.startsAt;
123
+ if (args.endsAt) body.endsAt = args.endsAt;
124
+ if (args.maxRedemptions !== undefined) body.maxRedemptions = args.maxRedemptions;
125
+ if (args.campaign) body.campaign = args.campaign;
126
+
127
+ console.log(`\n🎟️ Creating discount code on ${args.env.toUpperCase()}...`);
128
+ const res = await callBilling(args.env, 'POST', '/api/billing/admin/discount-codes', body);
129
+ console.log(`✓ Created (HTTP ${res.status}):`);
130
+ console.log(JSON.stringify(res.body, null, 2));
131
+ }
132
+ ```
133
+
134
+ - [ ] **Step 2: `bin/helpers/discount/generate.ts`** (batch → write codes to file, not stdout)
135
+
136
+ ```ts
137
+ import * as fs from 'fs';
138
+ import { callBilling, validateEnv } from '../billing-http';
139
+ import { confirmIfProd } from '../confirm-prompt';
140
+
141
+ interface GenArgs {
142
+ count: number;
143
+ percentOff: number;
144
+ campaign: string;
145
+ productKeys?: string[];
146
+ startsAt?: string;
147
+ endsAt?: string;
148
+ env: string;
149
+ yes: boolean;
150
+ }
151
+
152
+ function toIso(v: string): string {
153
+ const d = new Date(v);
154
+ if (isNaN(d.getTime())) throw new Error(`Invalid date: ${v} (use YYYY-MM-DD or ISO datetime)`);
155
+ return d.toISOString();
156
+ }
157
+
158
+ function parseArgs(argv: string[]): GenArgs {
159
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
160
+ console.log(`Usage: optima-discount generate --count <N> --percent <1-100> --campaign <label> [options]
161
+
162
+ Generates N unique single-use codes (maxRedemptions=1). Codes are written to a
163
+ local file (NOT printed) to keep them copy-paste clean.
164
+
165
+ Required:
166
+ --count <N> How many codes (1-1000)
167
+ --percent <1-100> Percentage off
168
+ --campaign <label> Grouping label (also the code prefix)
169
+
170
+ Optional:
171
+ --products <a,b,...> Limit to these productKeys
172
+ --starts <date> Valid-from (YYYY-MM-DD or ISO)
173
+ --ends <date> Valid-until
174
+ --env stage|prod (default: stage)
175
+ --yes Skip prod confirmation`);
176
+ process.exit(0);
177
+ }
178
+ const out: Partial<GenArgs> = { env: 'stage', yes: false };
179
+ for (let i = 0; i < argv.length; i++) {
180
+ const a = argv[i];
181
+ const next = argv[i + 1];
182
+ switch (a) {
183
+ case '--count': out.count = parseInt(next, 10); i++; break;
184
+ case '--percent': out.percentOff = parseInt(next, 10); i++; break;
185
+ case '--campaign': out.campaign = next; i++; break;
186
+ case '--products': out.productKeys = next.split(',').map((s) => s.trim()).filter(Boolean); i++; break;
187
+ case '--starts': out.startsAt = toIso(next); i++; break;
188
+ case '--ends': out.endsAt = toIso(next); i++; break;
189
+ case '--env': out.env = next; i++; break;
190
+ case '--yes': out.yes = true; break;
191
+ default: throw new Error(`Unknown arg: ${a}`);
192
+ }
193
+ }
194
+ if (out.count === undefined || isNaN(out.count)) throw new Error('--count required (1-1000)');
195
+ if (out.percentOff === undefined || isNaN(out.percentOff)) throw new Error('--percent required (1-100)');
196
+ if (!out.campaign) throw new Error('--campaign required');
197
+ return out as GenArgs;
198
+ }
199
+
200
+ export async function runGenerate(argv: string[]): Promise<void> {
201
+ const args = parseArgs(argv);
202
+ validateEnv(args.env);
203
+ await confirmIfProd(args.env, `Generate ${args.count} unique discount codes (${args.percentOff}% off, campaign ${args.campaign})`, args.yes);
204
+
205
+ const body: Record<string, unknown> = { count: args.count, percentOff: args.percentOff, campaign: args.campaign };
206
+ if (args.productKeys) body.productKeys = args.productKeys;
207
+ if (args.startsAt) body.startsAt = args.startsAt;
208
+ if (args.endsAt) body.endsAt = args.endsAt;
209
+
210
+ console.log(`\n🎟️ Generating ${args.count} codes on ${args.env.toUpperCase()}...`);
211
+ const res = await callBilling<{ codes: string[] }>(args.env, 'POST', '/api/billing/admin/discount-codes/batch', body);
212
+ const codes = res.body.codes ?? [];
213
+
214
+ const safeCampaign = args.campaign.replace(/[^A-Za-z0-9_-]/g, '');
215
+ const file = `./discount-codes-${safeCampaign}-${Date.now()}.txt`;
216
+ // mode 0o600: single-use codes are sensitive-ish; written to the operator's CWD.
217
+ fs.writeFileSync(file, codes.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600 });
218
+ console.log(`✓ Generated ${codes.length} codes (HTTP ${res.status}). Written to: ${file}`);
219
+ console.log(` (codes are in the file, not printed, to keep them copy-paste clean)`);
220
+ }
221
+ ```
222
+
223
+ - [ ] **Step 3: `bin/helpers/discount/list.ts`**
224
+
225
+ ```ts
226
+ import { callBilling, validateEnv } from '../billing-http';
227
+
228
+ interface ListArgs {
229
+ campaign?: string;
230
+ code?: string;
231
+ limit?: number;
232
+ env: string;
233
+ }
234
+
235
+ function parseArgs(argv: string[]): ListArgs {
236
+ if (argv[0] === '-h' || argv[0] === '--help') {
237
+ console.log(`Usage: optima-discount list [options]
238
+
239
+ Optional:
240
+ --campaign <label> Filter by campaign
241
+ --code <CODE> Filter by exact code
242
+ --limit <N> Max rows (default 500, max 1000)
243
+ --env stage|prod (default: stage)`);
244
+ process.exit(0);
245
+ }
246
+ const out: Partial<ListArgs> = { env: 'stage' };
247
+ for (let i = 0; i < argv.length; i++) {
248
+ const a = argv[i];
249
+ const next = argv[i + 1];
250
+ switch (a) {
251
+ case '--campaign': out.campaign = next; i++; break;
252
+ case '--code': out.code = next; i++; break;
253
+ case '--limit': out.limit = parseInt(next, 10); i++; break;
254
+ case '--env': out.env = next; i++; break;
255
+ default: throw new Error(`Unknown arg: ${a}`);
256
+ }
257
+ }
258
+ return out as ListArgs;
259
+ }
260
+
261
+ export async function runList(argv: string[]): Promise<void> {
262
+ const args = parseArgs(argv);
263
+ validateEnv(args.env);
264
+ const qs = new URLSearchParams();
265
+ if (args.campaign) qs.set('campaign', args.campaign);
266
+ if (args.code) qs.set('code', args.code);
267
+ if (args.limit !== undefined) qs.set('limit', String(args.limit));
268
+ const suffix = qs.toString() ? `?${qs.toString()}` : '';
269
+ const res = await callBilling(args.env, 'GET', `/api/billing/admin/discount-codes${suffix}`);
270
+ console.log(JSON.stringify(res.body, null, 2));
271
+ }
272
+ ```
273
+
274
+ - [ ] **Step 4: `bin/helpers/discount/disable.ts`**
275
+
276
+ ```ts
277
+ import { callBilling, validateEnv } from '../billing-http';
278
+ import { confirmIfProd } from '../confirm-prompt';
279
+
280
+ interface DisableArgs {
281
+ code: string;
282
+ env: string;
283
+ yes: boolean;
284
+ }
285
+
286
+ function parseArgs(argv: string[]): DisableArgs {
287
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
288
+ console.log(`Usage: optima-discount disable --code <CODE> [options]
289
+
290
+ Required:
291
+ --code <CODE>
292
+
293
+ Optional:
294
+ --env stage|prod (default: stage)
295
+ --yes Skip prod confirmation`);
296
+ process.exit(0);
297
+ }
298
+ const out: Partial<DisableArgs> = { env: 'stage', yes: false };
299
+ for (let i = 0; i < argv.length; i++) {
300
+ const a = argv[i];
301
+ const next = argv[i + 1];
302
+ switch (a) {
303
+ case '--code': out.code = next; i++; break;
304
+ case '--env': out.env = next; i++; break;
305
+ case '--yes': out.yes = true; break;
306
+ default: throw new Error(`Unknown arg: ${a}`);
307
+ }
308
+ }
309
+ if (!out.code) throw new Error('--code required');
310
+ return out as DisableArgs;
311
+ }
312
+
313
+ export async function runDisable(argv: string[]): Promise<void> {
314
+ const args = parseArgs(argv);
315
+ validateEnv(args.env);
316
+ await confirmIfProd(args.env, `Disable discount code ${args.code.toUpperCase()}`, args.yes);
317
+ const res = await callBilling(args.env, 'PATCH', `/api/billing/admin/discount-codes/${encodeURIComponent(args.code)}`, { status: 'DISABLED' });
318
+ console.log(`✓ Disabled (HTTP ${res.status}):`);
319
+ console.log(JSON.stringify(res.body, null, 2));
320
+ }
321
+ ```
322
+
323
+ - [ ] **Step 5: dispatcher `bin/helpers/discount.ts`**
324
+
325
+ ```ts
326
+ #!/usr/bin/env node
327
+
328
+ import { runCreate } from './discount/create';
329
+ import { runGenerate } from './discount/generate';
330
+ import { runList } from './discount/list';
331
+ import { runDisable } from './discount/disable';
332
+
333
+ function printHelp() {
334
+ console.log(`Usage: optima-discount <subcommand> [options]
335
+
336
+ Subcommands:
337
+ create Create one discount code (shared or single-use)
338
+ generate Generate N unique single-use codes (written to a file)
339
+ list List discount codes (filter by campaign/code)
340
+ disable Disable a discount code
341
+
342
+ Run 'optima-discount <subcommand> --help' for subcommand-specific options.`);
343
+ }
344
+
345
+ async function main() {
346
+ const [, , subcommand, ...rest] = process.argv;
347
+ if (!subcommand || subcommand === '-h' || subcommand === '--help') {
348
+ printHelp();
349
+ process.exit(0);
350
+ }
351
+ switch (subcommand) {
352
+ case 'create': await runCreate(rest); break;
353
+ case 'generate': await runGenerate(rest); break;
354
+ case 'list': await runList(rest); break;
355
+ case 'disable': await runDisable(rest); break;
356
+ default:
357
+ console.error(`Unknown subcommand: ${subcommand}`);
358
+ printHelp();
359
+ process.exit(1);
360
+ }
361
+ }
362
+
363
+ main().catch((err) => {
364
+ console.error(err.message);
365
+ process.exit(1);
366
+ });
367
+ ```
368
+
369
+ - [ ] **Step 6: register bin in `package.json`**
370
+
371
+ Add to the `"bin"` object (alphabetical-ish, near `optima-... `): `"optima-discount": "dist/bin/helpers/discount.js"`. Verify valid JSON (`python3 -c "import json;json.load(open('package.json'))"`).
372
+
373
+ - [ ] **Step 7: build (type-check all of bin/)**
374
+
375
+ Run: `cd /mnt/d/work/projects/optima-4/optima-dev-skills/.worktrees/feat/discount-cli && npm run build`
376
+ Expected: tsc exits 0 (compiles `bin/helpers/discount.ts` + the 4 subcommand files to `dist/`). Confirm `dist/bin/helpers/discount.js` exists.
377
+
378
+ - [ ] **Step 8: smoke `--help` (no network)**
379
+
380
+ Run: `node dist/bin/helpers/discount.js --help` and `node dist/bin/helpers/discount.js create --help`
381
+ Expected: prints usage, exits 0 (help path doesn't call billing).
382
+
383
+ - [ ] **Step 9: Commit**
384
+
385
+ ```bash
386
+ cd /mnt/d/work/projects/optima-4/optima-dev-skills/.worktrees/feat/discount-cli
387
+ npm run build
388
+ git add bin/helpers/discount.ts bin/helpers/discount package.json .gitignore
389
+ git commit -m "feat(discount): optima-discount CLI (create/generate/list/disable)
390
+
391
+ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Task 2: SKILL.md + README
397
+
398
+ **Files:** Create `.claude/skills/discount-codes/SKILL.md`; Modify `README.md`.
399
+
400
+ - [ ] **Step 1: `.claude/skills/discount-codes/SKILL.md`**
401
+
402
+ ```markdown
403
+ ---
404
+ name: "discount-codes"
405
+ description: "当用户请求创建优惠码、发优惠码、生成折扣码、promo code、discount code、批量生成优惠码、停用优惠码、查看优惠码时,使用此技能。支持 Stage、Prod 两个环境。"
406
+ allowed-tools: ["Bash"]
407
+ ---
408
+
409
+ # 优惠码管理
410
+
411
+ 为 billing 结账(技能包 / 会员)创建和管理百分比优惠码。
412
+
413
+ ## 执行方式:使用 CLI 工具
414
+
415
+ 无论用户用 `/discount-codes` 还是直接请求,都使用 `optima-discount` CLI(thin HTTP client,调 billing admin 端点):
416
+
417
+ \`\`\`bash
418
+ optima-discount <subcommand> [options]
419
+ \`\`\`
420
+
421
+ ## 子命令
422
+
423
+ \`\`\`bash
424
+ # 建共享码:LAUNCH20 = 8 折,限 scout,6/30 截止,最多核销 100 次
425
+ optima-discount create --code LAUNCH20 --percent 20 --products scout --ends 2026-06-30 --max 100 --env prod
426
+
427
+ # 批量唯一码(每码用一次)——码写入本地文件,不打屏
428
+ optima-discount generate --count 100 --percent 50 --campaign partner-q3 --products scout --env prod
429
+ # → ./discount-codes-partner-q3-<ts>.txt
430
+
431
+ # 查看(按 campaign / code 过滤)
432
+ optima-discount list --campaign partner-q3 --env prod
433
+
434
+ # 停用
435
+ optima-discount disable --code LAUNCH20 --env prod
436
+ \`\`\`
437
+
438
+ ## 参数
439
+
440
+ | 参数 | 说明 |
441
+ |------|------|
442
+ | `--code` | 优惠码(存为大写) |
443
+ | `--percent` | 折扣百分比 1-100 |
444
+ | `--products` | 逗号分隔的 productKey;省略=所有商品 |
445
+ | `--starts` / `--ends` | 有效期(YYYY-MM-DD 或 ISO datetime) |
446
+ | `--max` | 总核销上限(省略=不限;1=一次性) |
447
+ | `--campaign` | 分组标签(generate 时也是码前缀) |
448
+ | `--count` | generate 生成数量 1-1000 |
449
+ | `--limit` | list 返回上限(默认 500,最大 1000) |
450
+ | `--env` | stage / prod(默认 stage) |
451
+ | `--yes` | 跳过 prod 二次确认 |
452
+
453
+ ## 安全提醒
454
+
455
+ 1. **Stage 优先**:默认 stage。
456
+ 2. **Prod 谨慎**:create / generate / disable 在 prod 会要求输入 "yes" 确认(`--yes` 跳过)。
457
+ 3. **唯一码写文件**:`generate` 的码写入当前目录文件,不打屏(避免复制时被终端 padding 破坏)。
458
+ 4. 依赖 billing 已部署对应环境(admin 端点存在)。
459
+
460
+ ## 相关
461
+
462
+ - `optima-product` — 管理付费商品(优惠码作用于其结账)
463
+ - `optima-query-db` — 查 discount_codes / discount_redemptions 表核对
464
+ ```
465
+
466
+ > 注意:上面 SKILL.md 正文里的 ``` 围栏在真实文件中是三反引号;写文件时不要转义。
467
+
468
+ - [ ] **Step 2: README — skill 列表 + CLI 表**
469
+
470
+ 在 README.md 的 skill 任务场景列表(`- **read-code** - ...` 附近)加一行:
471
+ ```
472
+ - **discount-codes** - 创建/生成/查看/停用 billing 优惠码(Stage/Prod)
473
+ ```
474
+ 在 CLI 工具表(`| optima-query-db | ... |` 附近)加一行:
475
+ ```
476
+ | `optima-discount` | 优惠码管理 | `optima-discount create --code LAUNCH20 --percent 20 --env stage` |
477
+ ```
478
+
479
+ - [ ] **Step 3: build sanity**
480
+
481
+ Run: `cd /mnt/d/work/projects/optima-4/optima-dev-skills/.worktrees/feat/discount-cli && npm run build`
482
+ Expected: 0 errors(SKILL.md/README 不参与编译,但确认没碰坏 bin/)。
483
+
484
+ - [ ] **Step 4: Commit**
485
+
486
+ ```bash
487
+ cd /mnt/d/work/projects/optima-4/optima-dev-skills/.worktrees/feat/discount-cli
488
+ git add .claude/skills/discount-codes/SKILL.md README.md
489
+ git commit -m "docs(discount): discount-codes SKILL.md + README
490
+
491
+ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
492
+ ```
493
+
494
+ ---
495
+
496
+ ## 收尾:build + push + PR
497
+
498
+ - [ ] **Step 1:** `npm run build` 全绿 + `node dist/bin/helpers/discount.js --help` 正常。
499
+ - [ ] **Step 2: 端到端验证(部署后,本地不可做)** —— billing 部署到 stage 后,用 `optima-discount create --code SMOKE10 --percent 10 --env stage` 真发一个码,确认 201;`list --code SMOKE10 --env stage` 能查到;`disable --code SMOKE10 --env stage` 生效。需 dev-skills service client 能拿 billing OAuth token(`callBilling` 已有)。记录结果。
500
+ - [ ] **Step 3: push + PR(base main)**
501
+ ```bash
502
+ cd /mnt/d/work/projects/optima-4/optima-dev-skills/.worktrees/feat/discount-cli
503
+ git push -u origin feat/discount-cli
504
+ gh pr create --base main --title "feat: optima-discount CLI (优惠码发码工具)" --body "Plan B —— 调 billing admin 端点(billing#65 已 merge)的发码 CLI。E2E 需 billing 部署 stage 后验。"
505
+ ```
506
+
507
+ ---
508
+
509
+ ## Plan B ↔ Spec 覆盖
510
+
511
+ | Spec | Task |
512
+ |---|---|
513
+ | §6 CLI(create/generate/list/disable,HTTP,dispatcher+subcommand,码写文件,confirmIfProd)| Task 1 |
514
+ | §6 SKILL.md + README | Task 2 |
515
+ | §4.6 端点契约(消费方)| Task 1(billing 侧已 Plan A 实现)|
516
+
517
+ **不在 Plan B**:Plan C = agentic-chat 前端(CheckoutModal/ProviderSelector + i18n,base `revert/pre-plan-d`)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.7.36",
3
+ "version": "0.7.37",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "optima-grant-balance": "dist/bin/helpers/grant-balance.js",
11
11
  "optima-grant-subscription": "dist/bin/helpers/grant-subscription.js",
12
12
  "optima-plugin": "dist/bin/helpers/plugin.js",
13
+ "optima-discount": "dist/bin/helpers/discount.js",
13
14
  "optima-product": "dist/bin/helpers/product.js",
14
15
  "optima-query-db": "dist/bin/helpers/query-db.js",
15
16
  "optima-show-env": "dist/bin/helpers/show-env.js"