diff --git a/packages/cli/src/commands/iterate-options.ts b/packages/cli/src/commands/iterate-options.ts new file mode 100644 index 00000000..200575b4 --- /dev/null +++ b/packages/cli/src/commands/iterate-options.ts @@ -0,0 +1,22 @@ +import { InvalidArgumentError } from 'commander'; + +export function parsePositiveSafeInteger(value: string): number { + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed < 1) { + throw new InvalidArgumentError('must be a positive safe integer'); + } + return parsed; +} + +export function parseQuietHours(value: string): string { + const match = value.match(/^(\d{1,2})-(\d{1,2})$/); + if (!match) { + throw new InvalidArgumentError('must use start-end format, for example 22-08'); + } + const start = Number(match[1]); + const end = Number(match[2]); + if (start > 23 || end > 23) { + throw new InvalidArgumentError('hours must be between 0 and 23'); + } + return value; +} diff --git a/packages/cli/src/commands/iterate.test.ts b/packages/cli/src/commands/iterate.test.ts new file mode 100644 index 00000000..4892cab6 --- /dev/null +++ b/packages/cli/src/commands/iterate.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { parsePositiveSafeInteger, parseQuietHours } from './iterate-options.js'; + +describe('parsePositiveSafeInteger', () => { + it('accepts positive safe integer intervals', () => { + expect(parsePositiveSafeInteger('60')).toBe(60); + }); + + it.each(['nope', '0', '-1', '1.5', 'Infinity', '9007199254740992'])( + 'rejects invalid interval %s', + (value) => { + expect(() => parsePositiveSafeInteger(value)).toThrow('positive safe integer'); + }, + ); +}); + +describe('parseQuietHours', () => { + it.each(['22-08', '0-23', '09-17'])('accepts valid local hour range %s', (value) => { + expect(parseQuietHours(value)).toBe(value); + }); + + it.each(['abc', '22', '22:08', '22-8-1', '-1-08'])( + 'rejects invalid quiet-hours format %s', + (value) => { + expect(() => parseQuietHours(value)).toThrow('start-end format'); + }, + ); + + it.each(['24-08', '22-24', '23-99'])('rejects out-of-range quiet hours %s', (value) => { + expect(() => parseQuietHours(value)).toThrow('between 0 and 23'); + }); +}); diff --git a/packages/cli/src/commands/iterate.ts b/packages/cli/src/commands/iterate.ts index 14f09aee..15bdcc05 100644 --- a/packages/cli/src/commands/iterate.ts +++ b/packages/cli/src/commands/iterate.ts @@ -6,6 +6,7 @@ import { randomBytes } from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { configDir } from '@profullstack/sh1pt-core'; import { describeInput, resolveInput } from '../input.js'; +import { parsePositiveSafeInteger, parseQuietHours } from './iterate-options.js'; // agentsCmd moved to root level — see https://github.com/profullstack/sh1pt/issues/235 @@ -293,8 +294,8 @@ iterateCmd .option('--agent ', 'claude | codex | qwen', 'claude') .option('--scope ', 'copy | pricing | onboarding | perf | bugs | all', 'all') .option('--cloud', 'schedule in sh1pt cloud instead of local cron') - .option('--interval ', 're-check interval in seconds', Number, 3600) - .option('--quiet-hours ', 'pause during these local hours, e.g. 22-08') + .option('--interval ', 're-check interval in seconds', parsePositiveSafeInteger, 3600) + .option('--quiet-hours ', 'pause during these local hours, e.g. 22-08', parseQuietHours) .option('--stop', 'remove the watch configuration') .option('--status', 'show current watch configuration') .action(async (opts: {