Skip to content
Merged
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
22 changes: 22 additions & 0 deletions packages/cli/src/commands/iterate-options.ts
Original file line number Diff line number Diff line change
@@ -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})$/);
Comment on lines +10 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 start === end accepted but creates a permanently-silent quiet window

When start equals end (e.g., 10-10), the validator passes (both values ≤ 23), but inQuietHours evaluates hour >= 10 && hour < 10 — a contradiction that is never true. The watch daemon will never pause, with no error or warning surfaced to the user. Adding if (start === end) throw new InvalidArgumentError('start and end hours must differ'); before returning value would close this gap.

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;
}
32 changes: 32 additions & 0 deletions packages/cli/src/commands/iterate.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 3 additions & 2 deletions packages/cli/src/commands/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -293,8 +294,8 @@ iterateCmd
.option('--agent <id>', 'claude | codex | qwen', 'claude')
.option('--scope <area>', 'copy | pricing | onboarding | perf | bugs | all', 'all')
.option('--cloud', 'schedule in sh1pt cloud instead of local cron')
.option('--interval <seconds>', 're-check interval in seconds', Number, 3600)
.option('--quiet-hours <start-end>', 'pause during these local hours, e.g. 22-08')
.option('--interval <seconds>', 're-check interval in seconds', parsePositiveSafeInteger, 3600)
.option('--quiet-hours <start-end>', '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: {
Expand Down
Loading