diff --git a/skills/sub2api-admin/SKILL.md b/skills/sub2api-admin/SKILL.md index 61287e3916..7fcb17f369 100644 --- a/skills/sub2api-admin/SKILL.md +++ b/skills/sub2api-admin/SKILL.md @@ -1,17 +1,16 @@ --- name: sub2api-admin -description: Manage Sub2API admin APIs for accounts, groups, proxies, error passthrough rules, TLS fingerprint profiles, imports, exports, batch updates, and raw administrator API calls. Use when the user mentions Sub2API, admin API keys, account management, bulk account import/export, keeping or deleting accounts, refreshing accounts, clearing errors, CRS sync, or managing Sub2API backend settings through the admin API. +description: Manage Sub2API admin APIs for accounts, redeem codes, groups, proxies, error passthrough rules, TLS fingerprint profiles, imports, exports, batch updates, and raw administrator API calls. Use when the user mentions Sub2API, admin API keys, account management, redeem code management, recharge codes, invitation codes, bulk account import/export, keeping or deleting accounts, refreshing accounts, clearing errors, CRS sync, or managing Sub2API backend settings through the admin API. --- # Sub2API Admin -Use the bundled CLI instead of ad hoc `curl`. +Use the bundled CLI instead of ad hoc `curl`. Run examples from this skill directory. ```bash export SUB2API_BASE_URL='https://your-sub2api-host' export SUB2API_ADMIN_API_KEY='' - -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list +node scripts/sub2api-admin.js accounts list ``` For all commands and payload examples, read [references/admin-cli.md](references/admin-cli.md). @@ -27,13 +26,16 @@ For all commands and payload examples, read [references/admin-cli.md](references ## Common Commands ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10}' -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list +node scripts/sub2api-admin.js accounts list --page-size 20 +node scripts/sub2api-admin.js accounts get 40 +node scripts/sub2api-admin.js accounts usage 40 +node scripts/sub2api-admin.js accounts set-schedulable 40 true +node scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10}' +node scripts/sub2api-admin.js redeem-codes list --page-size 20 +node scripts/sub2api-admin.js redeem-codes generate --json '{"count":1,"type":"balance","value":10}' --idempotency-key redeem-$(date +%s) +node scripts/sub2api-admin.js redeem-codes create-and-redeem --json '{"code":"order_123","type":"balance","value":10,"user_id":123}' --idempotency-key order-123 +node scripts/sub2api-admin.js error-rules list +node scripts/sub2api-admin.js tls-profiles list ``` ## Safety Notes @@ -41,4 +43,5 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list - Authentication uses only `x-api-key`. - If the API returns `INVALID_ADMIN_KEY`, ask the user to regenerate the admin API key. - `accounts export` includes credentials and tokens. Prefer `--file` and avoid printing exports in chat. +- Redeem code create/redeem commands should use `--idempotency-key` for payment or recharge workflows. - For uncertain or newly added backend APIs, use `api ` after a read-only check. diff --git a/skills/sub2api-admin/references/admin-cli.md b/skills/sub2api-admin/references/admin-cli.md index c39e405906..37a48066f7 100644 --- a/skills/sub2api-admin/references/admin-cli.md +++ b/skills/sub2api-admin/references/admin-cli.md @@ -11,8 +11,10 @@ export SUB2API_ADMIN_API_KEY='' ## CLI +以下命令都假设当前目录是这个 skill 目录。 + ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js +node scripts/sub2api-admin.js ``` ## Accounts @@ -20,41 +22,41 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js ### 只读 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --search outlook --platform openai --type oauth --status active -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts stats 40 --days 30 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts today-stats 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-today-stats --ids 40,39 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts models 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts temp-unschedulable 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts antigravity-default-model-mapping +node scripts/sub2api-admin.js accounts list --page-size 20 +node scripts/sub2api-admin.js accounts list --search outlook --platform openai --type oauth --status active +node scripts/sub2api-admin.js accounts get 40 +node scripts/sub2api-admin.js accounts usage 40 +node scripts/sub2api-admin.js accounts stats 40 --days 30 +node scripts/sub2api-admin.js accounts today-stats 40 +node scripts/sub2api-admin.js accounts batch-today-stats --ids 40,39 +node scripts/sub2api-admin.js accounts models 40 +node scripts/sub2api-admin.js accounts temp-unschedulable 40 +node scripts/sub2api-admin.js accounts antigravity-default-model-mapping ``` `accounts export` 会包含账号凭据和 token,建议写入文件,不要直接刷屏: ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --ids 40,39 --file accounts-export.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --platform openai --type oauth --include-proxies false --file accounts-export.json +node scripts/sub2api-admin.js accounts export --ids 40,39 --file accounts-export.json +node scripts/sub2api-admin.js accounts export --platform openai --type oauth --include-proxies false --file accounts-export.json ``` ### 单账号写入 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts create --file account.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts update 40 --json '{"concurrency":20}' -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-status 40 active -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-error 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-rate-limit 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts recover-state 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-quota 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts refresh 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts test 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts sync-models 40 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts apply-oauth 40 --file credentials.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-temp-unschedulable 40 +node scripts/sub2api-admin.js accounts create --file account.json +node scripts/sub2api-admin.js accounts update 40 --json '{"concurrency":20}' +node scripts/sub2api-admin.js accounts set-status 40 active +node scripts/sub2api-admin.js accounts set-schedulable 40 true +node scripts/sub2api-admin.js accounts clear-error 40 +node scripts/sub2api-admin.js accounts clear-rate-limit 40 +node scripts/sub2api-admin.js accounts recover-state 40 +node scripts/sub2api-admin.js accounts reset-quota 40 +node scripts/sub2api-admin.js accounts refresh 40 +node scripts/sub2api-admin.js accounts test 40 +node scripts/sub2api-admin.js accounts sync-models 40 +node scripts/sub2api-admin.js accounts apply-oauth 40 --file credentials.json +node scripts/sub2api-admin.js accounts reset-temp-unschedulable 40 ``` ### 删除与清理 @@ -62,18 +64,18 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-temp- 删除前先列出目标账号名和 ID。 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts delete 25 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts keep-only --name 'target@example.com' +node scripts/sub2api-admin.js accounts delete 25 +node scripts/sub2api-admin.js accounts keep-only --name 'target@example.com' ``` ### 批量写入 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-create --file accounts.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-update-credentials --file payload.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10,"priority":2}' -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-refresh --ids 40,39 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-clear-error --ids 40,39 +node scripts/sub2api-admin.js accounts batch-create --file accounts.json +node scripts/sub2api-admin.js accounts batch-update-credentials --file payload.json +node scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10,"priority":2}' +node scripts/sub2api-admin.js accounts batch-refresh --ids 40,39 +node scripts/sub2api-admin.js accounts batch-clear-error --ids 40,39 ``` `bulk-update` 可覆盖页面“批量更新”的字段,payload 由后台表单字段决定,例如 `base_url`、`model_mapping`、`group_ids`、`proxy_id`、`concurrency`、`priority`、`rate_multiplier`、`status`、`compact_mode` 等。更新前先用 `accounts get ` 确认字段名。 @@ -83,21 +85,21 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-clear 通用后台导入: ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-data --file accounts-export.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-codex-session --file payload.json +node scripts/sub2api-admin.js accounts import-data --file accounts-export.json +node scripts/sub2api-admin.js accounts import-codex-session --file payload.json ``` CRS 同步: ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-preview --file payload.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-sync --file payload.json +node scripts/sub2api-admin.js accounts crs-preview --file payload.json +node scripts/sub2api-admin.js accounts crs-sync --file payload.json ``` 旧版 JSON 导入仍可用,会把模板账号的配置复制给导入账号: ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-json \ +node scripts/sub2api-admin.js accounts import-json \ --file /path/accounts.json \ --template-name 'template@example.com' \ --dry-run @@ -113,8 +115,59 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-json ## Groups And Proxies ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js groups all -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js proxies all +node scripts/sub2api-admin.js groups all +node scripts/sub2api-admin.js proxies all +``` + +## Redeem Codes + +兑换码类型包括 `balance`、`concurrency`、`subscription`、`invitation`。状态常用 `unused`、`used`、`expired`。 + +### 只读 + +```bash +node scripts/sub2api-admin.js redeem-codes list --page-size 20 +node scripts/sub2api-admin.js redeem-codes list --type balance --status unused --search user@example.com +node scripts/sub2api-admin.js redeem-codes get 123 +node scripts/sub2api-admin.js redeem-codes stats +node scripts/sub2api-admin.js redeem-codes export --file redeem-codes.csv +``` + +### 生成兑换码 + +```bash +node scripts/sub2api-admin.js redeem-codes generate \ + --json '{"count":1,"type":"balance","value":10}' \ + --idempotency-key "redeem-generate-$(date +%s)" +``` + +订阅兑换码需要 `group_id` 和非零 `validity_days`: + +```bash +node scripts/sub2api-admin.js redeem-codes generate \ + --json '{"count":1,"type":"subscription","value":0,"group_id":2,"validity_days":30}' \ + --idempotency-key "redeem-subscription-$(date +%s)" +``` + +### 创建并兑换 + +用于支付回调或人工充值,一步完成创建兑换码并兑换到用户。生产流程必须传稳定的 `--idempotency-key`。 + +```bash +node scripts/sub2api-admin.js redeem-codes create-and-redeem \ + --json '{"code":"order_123","type":"balance","value":10,"user_id":123,"notes":"manual recharge"}' \ + --idempotency-key order-123 +``` + +### 修改与清理 + +写入前先 `list` 或 `get` 核对目标 ID。 + +```bash +node scripts/sub2api-admin.js redeem-codes batch-update --ids 123,124 --json '{"notes":"campaign A"}' +node scripts/sub2api-admin.js redeem-codes expire 123 +node scripts/sub2api-admin.js redeem-codes delete 123 +node scripts/sub2api-admin.js redeem-codes batch-delete --ids 123,124 ``` ## Error Rules And TLS Profiles @@ -122,18 +175,18 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js proxies all 对应账号页顶部“错误透传规则”和“TLS 指纹模板”。 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules get 1 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules create --file rule.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules update 1 --json '{"enabled":true}' -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules toggle 1 false -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules delete 1 +node scripts/sub2api-admin.js error-rules list +node scripts/sub2api-admin.js error-rules get 1 +node scripts/sub2api-admin.js error-rules create --file rule.json +node scripts/sub2api-admin.js error-rules update 1 --json '{"enabled":true}' +node scripts/sub2api-admin.js error-rules toggle 1 false +node scripts/sub2api-admin.js error-rules delete 1 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles get 1 -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles create --file profile.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles update 1 --file profile.json -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles delete 1 +node scripts/sub2api-admin.js tls-profiles list +node scripts/sub2api-admin.js tls-profiles get 1 +node scripts/sub2api-admin.js tls-profiles create --file profile.json +node scripts/sub2api-admin.js tls-profiles update 1 --file profile.json +node scripts/sub2api-admin.js tls-profiles delete 1 ``` ## Raw Admin API @@ -141,8 +194,8 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles delete 未封装或新版本后台接口可用 `api` 直通。路径可写 `/admin/...` 或 `/api/v1/admin/...`。 ```bash -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api GET /admin/groups/all -node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api POST /admin/accounts/bulk-update \ +node scripts/sub2api-admin.js api GET /admin/groups/all +node scripts/sub2api-admin.js api POST /admin/accounts/bulk-update \ --json '{"account_ids":[40],"concurrency":10}' ``` @@ -183,6 +236,16 @@ node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api POST /admin/acco - `GET /api/v1/admin/accounts/antigravity/default-model-mapping` - `GET /api/v1/admin/groups/all` - `GET /api/v1/admin/proxies/all` +- `GET /api/v1/admin/redeem-codes` +- `GET /api/v1/admin/redeem-codes/export` +- `GET /api/v1/admin/redeem-codes/stats` +- `GET /api/v1/admin/redeem-codes/:id` +- `POST /api/v1/admin/redeem-codes/generate` +- `POST /api/v1/admin/redeem-codes/create-and-redeem` +- `POST /api/v1/admin/redeem-codes/batch-update` +- `POST /api/v1/admin/redeem-codes/batch-delete` +- `POST /api/v1/admin/redeem-codes/:id/expire` +- `DELETE /api/v1/admin/redeem-codes/:id` - `GET /api/v1/admin/error-passthrough-rules` - `GET /api/v1/admin/error-passthrough-rules/:id` - `POST /api/v1/admin/error-passthrough-rules` diff --git a/skills/sub2api-admin/scripts/sub2api-admin.js b/skills/sub2api-admin/scripts/sub2api-admin.js index 71b3609820..4ac5e38159 100755 --- a/skills/sub2api-admin/scripts/sub2api-admin.js +++ b/skills/sub2api-admin/scripts/sub2api-admin.js @@ -45,6 +45,16 @@ function usage() { sub2api-admin.js accounts import-json --file --template-name [--skip-name ] [--dry-run] sub2api-admin.js groups all sub2api-admin.js proxies all + sub2api-admin.js redeem-codes list [--page-size 200] [--page N] [--type balance] [--status unused] [--search TEXT] [--sort-by id] [--sort-order desc] + sub2api-admin.js redeem-codes export [--file redeem-codes.csv] [list filters...] + sub2api-admin.js redeem-codes get + sub2api-admin.js redeem-codes generate --json '{...}' | --file payload.json [--idempotency-key KEY] + sub2api-admin.js redeem-codes create-and-redeem --json '{...}' | --file payload.json [--idempotency-key KEY] + sub2api-admin.js redeem-codes batch-update --ids 1,2 --json '{...}' | --file fields.json + sub2api-admin.js redeem-codes delete + sub2api-admin.js redeem-codes batch-delete --ids 1,2 + sub2api-admin.js redeem-codes expire + sub2api-admin.js redeem-codes stats sub2api-admin.js error-rules list|get|create|update|delete|toggle ... sub2api-admin.js tls-profiles list|get|create|update|delete ... sub2api-admin.js api [--json '{...}' | --file payload.json] @@ -84,10 +94,11 @@ function authHeaders() { throw new Error("Missing SUB2API_ADMIN_API_KEY"); } -async function apiRequest(method, pathname, body) { +async function apiRequest(method, pathname, body, extraHeaders = {}) { const headers = { ...authHeaders(), Accept: "application/json", + ...extraHeaders, }; const options = { method, headers }; if (body !== undefined) { @@ -109,6 +120,32 @@ async function apiRequest(method, pathname, body) { return data.data; } +async function apiRawRequest(method, pathname, body, extraHeaders = {}) { + const headers = { + ...authHeaders(), + Accept: "*/*", + ...extraHeaders, + }; + const options = { method, headers }; + if (body !== undefined) { + headers["Content-Type"] = "application/json"; + options.body = JSON.stringify(body); + } + const res = await fetch(`${BASE_URL}${pathname}`, options); + const text = await res.text(); + if (!res.ok) { + let detail = res.statusText; + try { + const data = JSON.parse(text); + detail = data.message || data.code || detail; + } catch { + if (text) detail = text; + } + throw new Error(`${method} ${pathname} failed: ${detail}`); + } + return text; +} + function encodeQuery(params) { const pairs = []; for (const [key, value] of Object.entries(params)) { @@ -129,6 +166,14 @@ async function adminRequest(method, adminPath, body) { return apiRequest(method, normalizeAdminPath(adminPath), body); } +async function adminRequestWithHeaders(method, adminPath, body, headers = {}) { + return apiRequest(method, normalizeAdminPath(adminPath), body, headers); +} + +async function adminRawRequest(method, adminPath, body, headers = {}) { + return apiRawRequest(method, normalizeAdminPath(adminPath), body, headers); +} + async function listAccounts(options = {}) { const params = { page: options.page || 1, @@ -201,6 +246,36 @@ function accountListOptions(flags) { }; } +function redeemCodesListOptions(flags) { + return { + page: Number(flags.page || 1), + pageSize: Number(flags["page-size"] || 200), + type: flags.type, + status: flags.status, + search: flags.search, + sortBy: flags["sort-by"] || "id", + sortOrder: flags["sort-order"] || "desc", + }; +} + +function redeemCodesQuery(flags) { + const options = redeemCodesListOptions(flags); + return encodeQuery({ + page: options.page, + page_size: options.pageSize, + type: options.type, + status: options.status, + search: options.search, + sort_by: options.sortBy, + sort_order: options.sortOrder, + }); +} + +function idempotencyHeaders(flags) { + if (!flags["idempotency-key"]) return {}; + return { "Idempotency-Key": flags["idempotency-key"] }; +} + function printJson(data) { console.log(JSON.stringify(data, null, 2)); } @@ -530,6 +605,71 @@ async function commandProxies(args) { throw new Error(`unknown proxies subcommand: ${sub || "(missing)"}`); } +async function commandRedeemCodes(args) { + const sub = args.positional[1]; + if (sub === "list") { + printJson(await adminRequest("GET", `/admin/redeem-codes${redeemCodesQuery(args.flags)}`)); + return; + } + if (sub === "export") { + const csv = await adminRawRequest("GET", `/admin/redeem-codes/export${redeemCodesQuery(args.flags)}`); + if (args.flags.file) { + fs.writeFileSync(path.resolve(args.flags.file), csv); + printJson({ file: path.resolve(args.flags.file) }); + } else { + process.stdout.write(csv); + } + return; + } + if (sub === "get") { + const id = args.positional[2]; + if (!id) throw new Error("redeem-codes get requires "); + printJson(await adminRequest("GET", `/admin/redeem-codes/${id}`)); + return; + } + if (sub === "generate") { + printJson(await adminRequestWithHeaders("POST", "/admin/redeem-codes/generate", readJsonPayload(args.flags), idempotencyHeaders(args.flags))); + return; + } + if (sub === "create-and-redeem") { + printJson(await adminRequestWithHeaders("POST", "/admin/redeem-codes/create-and-redeem", readJsonPayload(args.flags), idempotencyHeaders(args.flags))); + return; + } + if (sub === "batch-update") { + const fields = readJsonPayload(args.flags, { required: false }) || {}; + const payload = args.flags.ids ? { ids: parseIds(args.flags.ids), fields } : fields; + if (!Array.isArray(payload.ids) || payload.ids.length === 0) { + throw new Error("redeem-codes batch-update requires --ids or payload.ids"); + } + if (!payload.fields || typeof payload.fields !== "object") { + throw new Error("redeem-codes batch-update requires fields"); + } + printJson(await adminRequest("POST", "/admin/redeem-codes/batch-update", payload)); + return; + } + if (sub === "delete") { + const id = args.positional[2]; + if (!id) throw new Error("redeem-codes delete requires "); + printJson(await adminRequest("DELETE", `/admin/redeem-codes/${id}`)); + return; + } + if (sub === "batch-delete") { + printJson(await adminRequest("POST", "/admin/redeem-codes/batch-delete", { ids: parseIds(args.flags.ids) })); + return; + } + if (sub === "expire") { + const id = args.positional[2]; + if (!id) throw new Error("redeem-codes expire requires "); + printJson(await adminRequest("POST", `/admin/redeem-codes/${id}/expire`)); + return; + } + if (sub === "stats") { + printJson(await adminRequest("GET", "/admin/redeem-codes/stats")); + return; + } + throw new Error(`unknown redeem-codes subcommand: ${sub || "(missing)"}`); +} + async function commandCrudResource(args, name, basePath, options = {}) { const sub = args.positional[1]; const id = args.positional[2]; @@ -592,6 +732,10 @@ async function main() { await commandProxies(args); return; } + if (root === "redeem-codes") { + await commandRedeemCodes(args); + return; + } if (root === "error-rules") { await commandCrudResource(args, "error-rules", "/admin/error-passthrough-rules", { toggle: true }); return;