Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/cli/src/commands/merch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { parsePositiveInteger, parsePositiveNumber } from './merch.js';

describe('merch numeric option parsers', () => {
it('accepts decimal positive integers', () => {
expect(parsePositiveInteger('1')).toBe(1);
expect(parsePositiveInteger('25')).toBe(25);
});

it.each(['0', '-1', '1.5', '1e2', '0x10', 'Infinity', 'NaN', 'many'])(
'rejects invalid positive integer %s',
(value) => {
expect(() => parsePositiveInteger(value)).toThrow('positive integer');
},
);

it('accepts decimal positive money values and percentages', () => {
expect(parsePositiveNumber('0.5')).toBe(0.5);
expect(parsePositiveNumber('19.99')).toBe(19.99);
expect(parsePositiveNumber('40')).toBe(40);
});

it.each(['0', '-1', '1e2', '0x10', 'Infinity', 'NaN', 'free'])(
'rejects invalid positive number %s',
(value) => {
expect(() => parsePositiveNumber(value)).toThrow('positive finite number');
},
);
});
32 changes: 27 additions & 5 deletions packages/cli/src/commands/merch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Command } from 'commander';
import { Command, InvalidArgumentError } from 'commander';
import kleur from 'kleur';

// Merch = swag. Shirts, stickers, hoodies, mugs, pens, notebooks, tote
Expand All @@ -8,6 +8,28 @@ export const merchCmd = new Command('merch')
.description('Print & ship swag via Printful / Printify — for sale or for free conference giveaways')
.action(() => { merchCmd.help(); });

export function parsePositiveInteger(value: string): number {
if (!/^\d+$/.test(value)) {
throw new InvalidArgumentError('must be a positive integer');
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new InvalidArgumentError('must be a positive integer');
}
return parsed;
}

export function parsePositiveNumber(value: string): number {
if (!/^\d+(?:\.\d+)?$/.test(value)) {
throw new InvalidArgumentError('must be a positive finite number');
}
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new InvalidArgumentError('must be a positive finite number');
}
return parsed;
}

merchCmd
.command('setup')
.description('Connect a POD provider (Printful, Printify) and optionally a storefront (Shopify, Etsy, Gumroad)')
Expand All @@ -23,8 +45,8 @@ merchCmd
.requiredOption('--products <kind...>', 'tshirt hoodie sticker mug pen notebook etc.')
.option('--colors <list>', 'comma-separated colors', 'black,white')
.option('--sizes <list>', 'comma-separated sizes (apparel only)', 'S,M,L,XL,XXL')
.option('--price <usd>', 'retail price in USD; omit to use provider suggestion', Number)
.option('--markup <percent>', 'margin over base cost if --price is omitted', Number, 40)
.option('--price <usd>', 'retail price in USD; omit to use provider suggestion', parsePositiveNumber)
.option('--markup <percent>', 'margin over base cost if --price is omitted', parsePositiveNumber, 40)
.option('--provider <id>', 'default: first configured')
.option('--dry-run')
.action((opts) => {
Expand Down Expand Up @@ -55,8 +77,8 @@ merchCmd
.description('Bulk ship swag for free (conference, hackathon, community giveaway — no storefront)')
.requiredOption('--sku <id...>', 'which SKU(s) to ship')
.requiredOption('--addresses <csvPath>', 'CSV with name,email,address1,city,region,zip,country columns')
.option('--quantity <n>', 'items per recipient', Number, 1)
.option('--budget-cap <usd>', 'abort if total exceeds this (strongly recommended)', Number)
.option('--quantity <n>', 'items per recipient', parsePositiveInteger, 1)
.option('--budget-cap <usd>', 'abort if total exceeds this (strongly recommended)', parsePositiveNumber)
.option('--dry-run')
.action((opts) => {
console.log(kleur.yellow(`[stub] merch giveaway ${JSON.stringify(opts)}`));
Expand Down
Loading