Skip to content

Commit 05ea5cf

Browse files
Validate iterate watch scheduling options (#746)
1 parent 65682b9 commit 05ea5cf

3 files changed

Lines changed: 57 additions & 2 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { InvalidArgumentError } from 'commander';
2+
3+
export function parsePositiveSafeInteger(value: string): number {
4+
const parsed = Number(value);
5+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
6+
throw new InvalidArgumentError('must be a positive safe integer');
7+
}
8+
return parsed;
9+
}
10+
11+
export function parseQuietHours(value: string): string {
12+
const match = value.match(/^(\d{1,2})-(\d{1,2})$/);
13+
if (!match) {
14+
throw new InvalidArgumentError('must use start-end format, for example 22-08');
15+
}
16+
const start = Number(match[1]);
17+
const end = Number(match[2]);
18+
if (start > 23 || end > 23) {
19+
throw new InvalidArgumentError('hours must be between 0 and 23');
20+
}
21+
return value;
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { parsePositiveSafeInteger, parseQuietHours } from './iterate-options.js';
3+
4+
describe('parsePositiveSafeInteger', () => {
5+
it('accepts positive safe integer intervals', () => {
6+
expect(parsePositiveSafeInteger('60')).toBe(60);
7+
});
8+
9+
it.each(['nope', '0', '-1', '1.5', 'Infinity', '9007199254740992'])(
10+
'rejects invalid interval %s',
11+
(value) => {
12+
expect(() => parsePositiveSafeInteger(value)).toThrow('positive safe integer');
13+
},
14+
);
15+
});
16+
17+
describe('parseQuietHours', () => {
18+
it.each(['22-08', '0-23', '09-17'])('accepts valid local hour range %s', (value) => {
19+
expect(parseQuietHours(value)).toBe(value);
20+
});
21+
22+
it.each(['abc', '22', '22:08', '22-8-1', '-1-08'])(
23+
'rejects invalid quiet-hours format %s',
24+
(value) => {
25+
expect(() => parseQuietHours(value)).toThrow('start-end format');
26+
},
27+
);
28+
29+
it.each(['24-08', '22-24', '23-99'])('rejects out-of-range quiet hours %s', (value) => {
30+
expect(() => parseQuietHours(value)).toThrow('between 0 and 23');
31+
});
32+
});

packages/cli/src/commands/iterate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { randomBytes } from 'node:crypto';
66
import { spawnSync } from 'node:child_process';
77
import { configDir } from '@profullstack/sh1pt-core';
88
import { describeInput, resolveInput } from '../input.js';
9+
import { parsePositiveSafeInteger, parseQuietHours } from './iterate-options.js';
910

1011
// agentsCmd moved to root level — see https://github.com/profullstack/sh1pt/issues/235
1112

@@ -293,8 +294,8 @@ iterateCmd
293294
.option('--agent <id>', 'claude | codex | qwen', 'claude')
294295
.option('--scope <area>', 'copy | pricing | onboarding | perf | bugs | all', 'all')
295296
.option('--cloud', 'schedule in sh1pt cloud instead of local cron')
296-
.option('--interval <seconds>', 're-check interval in seconds', Number, 3600)
297-
.option('--quiet-hours <start-end>', 'pause during these local hours, e.g. 22-08')
297+
.option('--interval <seconds>', 're-check interval in seconds', parsePositiveSafeInteger, 3600)
298+
.option('--quiet-hours <start-end>', 'pause during these local hours, e.g. 22-08', parseQuietHours)
298299
.option('--stop', 'remove the watch configuration')
299300
.option('--status', 'show current watch configuration')
300301
.action(async (opts: {

0 commit comments

Comments
 (0)