UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

398 lines (389 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseInteger = parseInteger; exports.validateCouponType = validateCouponType; exports.validateCouponStatus = validateCouponStatus; exports.validateOutputFormat = validateOutputFormat; exports.validateYn = validateYn; exports.parseCouponIds = parseCouponIds; exports.validateSize = validateSize; exports.registerCouponCommands = registerCouponCommands; const COUPON_NAME_MAX_LENGTH = 50; const BATCH_DELETE_MAX_IDS = 200; const PAGE_SIZE_MAX = 1000; const DEFAULT_TIMEOUT_MS = 30000; const coupon_handler_1 = require("../handlers/coupon.handler"); const manager_1 = require("../config/manager"); const auth_adapter_1 = require("../config/auth-adapter"); const errors_1 = require("../utils/errors"); async function loadAuthAndServiceConfig(parentOptions) { const authResult = auth_adapter_1.authAdapter.tryGetAuthConfig(parentOptions); if (!authResult) { throw new Error(auth_adapter_1.authAdapter.getStatusMessage(parentOptions)); } let configResult; try { configResult = await manager_1.configManager.load({ cliOptions: parentOptions, }); } catch (error) { if (error instanceof Error && error.message.includes('Auth configuration is incomplete')) { configResult = { config: { baseUrl: 'https://api.polyv.net', timeout: DEFAULT_TIMEOUT_MS, debug: false, }, }; } else { throw error; } } const serviceConfig = { baseUrl: configResult.config.baseUrl, timeout: configResult.config.timeout, debug: configResult.config.debug, }; const isVerbose = !!parentOptions['verbose']; if (isVerbose) { console.log(`🔐 Authentication Source: ${authResult.source}`); if (authResult.accountName) { console.log(`👤 Account: ${authResult.accountName}`); } console.log(''); } return { authConfig: authResult.config, serviceConfig, isVerbose, }; } function parseInteger(value) { if (!/^-?\d+$/.test(value)) { throw new Error(`"${value}" is not a valid integer`); } const parsed = parseInt(value, 10); if (!Number.isSafeInteger(parsed)) { throw new Error(`"${value}" is outside safe integer range`); } return parsed; } function validateCouponType(value) { if (!['MAX_OUT', 'DISCOUNT'].includes(value)) { throw new Error('Invalid coupon type. Must be MAX_OUT or DISCOUNT'); } return value; } function validateCouponStatus(value) { const validStatuses = ['NOT_START', 'GOING', 'FINISHED', 'INVALID']; if (!validStatuses.includes(value)) { throw new Error(`Invalid coupon status. Must be one of: ${validStatuses.join(', ')}`); } return value; } function validateOutputFormat(value) { if (!['table', 'json'].includes(value)) { throw new Error('Invalid output format. Must be "table" or "json"'); } return value; } function validateYn(value) { if (value !== 'Y' && value !== 'N') { throw new Error('Value must be Y or N'); } return value; } function parseCouponIds(value) { const ids = value.split(',').map((item) => item.trim()).filter(Boolean); if (ids.length === 0) { throw new Error('coupon IDs must not be empty'); } return ids; } function validateSize(value) { const parsed = parseInteger(value); if (parsed < 1 || parsed > PAGE_SIZE_MAX) { throw new Error(`Size must be between 1 and ${PAGE_SIZE_MAX}`); } return parsed; } function registerCouponCommands(program) { const couponCmd = program.command('coupon'); couponCmd.description('Manage coupons'); const addCmd = couponCmd .command('add') .description('Create a new coupon (满减券 or 折扣券)') .requiredOption('--name <name>', `coupon name (max ${COUPON_NAME_MAX_LENGTH} characters)`) .requiredOption('--type <type>', 'coupon type (MAX_OUT | DISCOUNT)', validateCouponType) .requiredOption('--availableAmount <number>', 'issue quantity (>= 0)', parseInteger) .requiredOption('--receiveStart <timestamp>', 'receive start time (13-bit ms timestamp)', parseInteger) .requiredOption('--receiveEnd <timestamp>', 'receive end time (13-bit ms timestamp)', parseInteger) .requiredOption('--useTimeType <type>', 'use time type (RANGE | DAY)') .option('--useStart <timestamp>', 'use start time (required when useTimeType=RANGE)', parseInteger) .option('--useEnd <timestamp>', 'use end time (required when useTimeType=RANGE)', parseInteger) .option('--dayOfUse <days>', 'days available (required when useTimeType=DAY)', parseInteger) .requiredOption('--condition <type>', 'rule condition (UNCONDITIONAL | FULL_REDUCE)') .option('--discount <value>', 'discount value for UNCONDITIONAL rule', parseInteger) .option('--full <amount>', 'minimum spend for FULL_REDUCE rule', parseInteger) .option('--reduce <amount>', 'discount amount for FULL_REDUCE rule', parseInteger) .requiredOption('--limitPerPerson <number>', 'max claims per person (-1 for unlimited)', parseInteger) .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const addOptions = { name: options.name, type: options.type, availableAmount: options.availableAmount, receiveStart: options.receiveStart, receiveEnd: options.receiveEnd, useTimeType: options.useTimeType, useStart: options.useStart, useEnd: options.useEnd, dayOfUse: options.dayOfUse, condition: options.condition, discount: options.discount, full: options.full, reduce: options.reduce, limitPerPerson: options.limitPerPerson, output: options.output, }; await couponHandler.addCoupon(addOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); addCmd.addHelpText('after', ` Examples: # Create a MAX_OUT coupon (满减券) with FULL_REDUCE condition $ polyv-live-cli coupon add --name "满100减20" --type MAX_OUT --availableAmount 100 \\ --receiveStart 1704067200000 --receiveEnd 1704153600000 \\ --useTimeType RANGE --useStart 1704067200000 --useEnd 1704758400000 \\ --condition FULL_REDUCE --full 100 --reduce 20 --limitPerPerson 1 # Create a DISCOUNT coupon (折扣券) with UNCONDITIONAL condition $ polyv-live-cli coupon add --name "8折优惠券" --type DISCOUNT --availableAmount 200 \\ --receiveStart 1704067200000 --receiveEnd 1704153600000 \\ --useTimeType DAY --dayOfUse 7 \\ --condition UNCONDITIONAL --discount 80 --limitPerPerson 1 Coupon Types: MAX_OUT - Fixed amount discount (满减券) DISCOUNT - Percentage discount (折扣券) Use Time Types: RANGE - Specific time range (requires --useStart and --useEnd) DAY - Days after receive (requires --dayOfUse) Rule Conditions: UNCONDITIONAL - No minimum spend required (use --discount) FULL_REDUCE - Minimum spend required (use --full and --reduce) `); const listCmd = couponCmd .command('list') .description('List coupons with pagination and status filter') .option('-p, --page <number>', 'page number (minimum 1)', parseInteger, 1) .option('-s, --size <number>', 'items per page (1-1000)', validateSize, 10) .option('--status <status>', 'filter by status (NOT_START|GOING|FINISHED|INVALID)', validateCouponStatus) .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const listOptions = { page: options.page, size: options.size, status: options.status, output: options.output, }; await couponHandler.listCoupons(listOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); listCmd.addHelpText('after', ` Examples: $ polyv-live-cli coupon list $ polyv-live-cli coupon list -p 2 -s 20 $ polyv-live-cli coupon list --status GOING $ polyv-live-cli coupon list -o json Pagination: --page, -p Page number (minimum 1, default 1) --size, -s Items per page (1-1000, default 10) Status Filters: NOT_START - Coupons not yet started GOING - Active coupons FINISHED - Finished coupons INVALID - Invalid/disabled coupons Output Formats: table - Formatted table output (default) json - JSON format for programmatic use `); const deleteCmd = couponCmd .command('delete') .description(`Delete coupons in batch (max ${BATCH_DELETE_MAX_IDS} IDs)`) .requiredOption('--couponIds <ids...>', `coupon IDs to delete (max ${BATCH_DELETE_MAX_IDS})`, (value, previous = []) => { return previous.concat([value]); }) .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { if (options.couponIds.length > BATCH_DELETE_MAX_IDS) { throw new Error(`Maximum ${BATCH_DELETE_MAX_IDS} coupon IDs allowed for batch delete`); } const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const deleteOptions = { couponIds: options.couponIds, output: options.output, }; await couponHandler.deleteCoupons(deleteOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); deleteCmd.addHelpText('after', ` Examples: # Delete single coupon $ polyv-live-cli coupon delete --couponIds coupon001 # Delete multiple coupons $ polyv-live-cli coupon delete --couponIds coupon001 coupon002 coupon003 Notes: - Maximum ${BATCH_DELETE_MAX_IDS} coupon IDs can be deleted in one batch - Deletion is permanent and cannot be undone `); const channelCmd = couponCmd .command('channel') .description('Manage channel coupon associations'); channelCmd .command('enabled') .description('Get channel coupon switch') .requiredOption('-c, --channel-id <channelId>', 'channel ID') .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); await couponHandler.getChannelCouponEnabled({ channelId: options.channelId, output: options.output, }); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); channelCmd .command('update-enabled') .description('Update channel coupon switch') .requiredOption('-c, --channel-id <channelId>', 'channel ID') .requiredOption('--enabled <yn>', 'enabled flag (Y|N)', validateYn) .option('-f, --force', 'skip confirmation prompt') .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const updateOptions = { channelId: options.channelId, enabled: options.enabled, force: options.force, output: options.output, }; await couponHandler.updateChannelCouponEnabled(updateOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); channelCmd .command('list') .description('List coupons associated with a channel') .requiredOption('-c, --channel-id <channelId>', 'channel ID') .option('-p, --page <number>', 'page number (minimum 1)', parseInteger, 1) .option('-s, --size <number>', 'items per page (1-1000)', validateSize, 10) .option('-n, --name <name>', 'coupon name filter') .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const listOptions = { channelId: options.channelId, page: options.page, size: options.size, name: options.name, output: options.output, }; await couponHandler.listChannelCoupons(listOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); channelCmd .command('add') .description('Add platform coupons to a channel') .requiredOption('-c, --channel-id <channelId>', 'channel ID') .requiredOption('--coupon-ids <ids>', 'coupon IDs, comma-separated (max 30)', parseCouponIds) .option('-f, --force', 'skip confirmation prompt') .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const addOptions = { channelId: options.channelId, couponIds: options.couponIds, force: options.force, output: options.output, }; await couponHandler.addChannelCoupons(addOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); channelCmd .command('delete') .description('Delete coupons from a channel') .requiredOption('-c, --channel-id <channelId>', 'channel ID') .requiredOption('--coupon-ids <ids>', 'coupon IDs, comma-separated (max 30)', parseCouponIds) .option('-f, --force', 'skip confirmation prompt') .option('-o, --output <format>', 'output format (table|json)', validateOutputFormat, 'table') .action(async (options) => { try { const parentOptions = program.opts(); const { authConfig, serviceConfig } = await loadAuthAndServiceConfig(parentOptions); const couponHandler = new coupon_handler_1.CouponHandler(authConfig, serviceConfig); const deleteOptions = { channelId: options.channelId, couponIds: options.couponIds, force: options.force, output: options.output, }; await couponHandler.deleteChannelCoupons(deleteOptions); } catch (error) { (0, errors_1.logError)(error instanceof Error ? error : new Error(String(error))); process.exit(1); } }); } //# sourceMappingURL=coupon.commands.js.map