polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
398 lines (389 loc) • 17.2 kB
JavaScript
"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