Skip to content

Commit f2e7ce4

Browse files
Validate deploy numeric options (#744)
1 parent f1b74ea commit f2e7ce4

2 files changed

Lines changed: 51 additions & 8 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { parsePositiveFiniteNumber, parsePositiveSafeInteger } from './deploy.js';
3+
4+
describe('deploy numeric option parsers', () => {
5+
it('accepts positive safe integer resource counts', () => {
6+
expect(parsePositiveSafeInteger('4')).toBe(4);
7+
});
8+
9+
it.each(['nope', '0', '-1', '1.5', 'Infinity', '9007199254740992'])(
10+
'rejects invalid resource count %s',
11+
(value) => {
12+
expect(() => parsePositiveSafeInteger(value)).toThrow('positive safe integer');
13+
},
14+
);
15+
16+
it('accepts positive finite memory and price values', () => {
17+
expect(parsePositiveFiniteNumber('0.5')).toBe(0.5);
18+
expect(parsePositiveFiniteNumber('12.75')).toBe(12.75);
19+
});
20+
21+
it.each(['nope', '0', '-1', 'NaN', 'Infinity', '-Infinity'])(
22+
'rejects invalid finite value %s',
23+
(value) => {
24+
expect(() => parsePositiveFiniteNumber(value)).toThrow('positive finite number');
25+
},
26+
);
27+
});

packages/cli/src/commands/deploy.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1-
import { Command } from 'commander';
1+
import { Command, InvalidArgumentError } from 'commander';
22
import kleur from 'kleur';
33

4+
export function parsePositiveSafeInteger(value: string): number {
5+
const parsed = Number(value);
6+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
7+
throw new InvalidArgumentError('must be a positive safe integer');
8+
}
9+
return parsed;
10+
}
11+
12+
export function parsePositiveFiniteNumber(value: string): number {
13+
const parsed = Number(value);
14+
if (!Number.isFinite(parsed) || parsed <= 0) {
15+
throw new InvalidArgumentError('must be a positive finite number');
16+
}
17+
return parsed;
18+
}
19+
420
export const deployCmd = new Command('deploy')
521
.description('Provision cloud infrastructure — VPS, GPU, bare metal, managed databases, object storage')
622
.action(() => {
@@ -21,10 +37,10 @@ deployCmd
2137
.command('quote')
2238
.description('Price-check a spec across every connected provider before provisioning')
2339
.requiredOption('--kind <kind>', 'cpu-vps | gpu | bare-metal | managed-db | block-storage | object-storage')
24-
.option('--cpu <n>', 'vCPU count', Number)
25-
.option('--memory <gb>', 'RAM in GB', Number)
40+
.option('--cpu <n>', 'vCPU count', parsePositiveSafeInteger)
41+
.option('--memory <gb>', 'RAM in GB', parsePositiveFiniteNumber)
2642
.option('--gpu <model>', 'GPU model, e.g. A100, H100, RTX-4090')
27-
.option('--gpu-count <n>', 'GPUs per instance', Number)
43+
.option('--gpu-count <n>', 'GPUs per instance', parsePositiveSafeInteger)
2844
.option('--region <id>')
2945
.option('--spot', 'accept interruptible / spot instances for lower price')
3046
.action((opts) => {
@@ -37,14 +53,14 @@ deployCmd
3753
.description('Spin up a new instance (WILL start billing — pair with a --max-hourly-price guardrail)')
3854
.requiredOption('--provider <id>', 'e.g. cloud-runpod, cloud-digitalocean')
3955
.requiredOption('--kind <kind>')
40-
.option('--cpu <n>', 'vCPU count', Number)
41-
.option('--memory <gb>', 'memory in GB', Number)
56+
.option('--cpu <n>', 'vCPU count', parsePositiveSafeInteger)
57+
.option('--memory <gb>', 'memory in GB', parsePositiveFiniteNumber)
4258
.option('--gpu <model>', 'GPU model, e.g. A100, H100')
43-
.option('--gpu-count <n>', 'number of GPUs', Number)
59+
.option('--gpu-count <n>', 'number of GPUs', parsePositiveSafeInteger)
4460
.option('--region <id>')
4561
.option('--image <name>')
4662
.option('--spot')
47-
.option('--max-hourly-price <usd>', 'abort if quote exceeds this (strongly recommended for GPU)', Number)
63+
.option('--max-hourly-price <usd>', 'abort if quote exceeds this (strongly recommended for GPU)', parsePositiveFiniteNumber)
4864
.option('--dry-run', 'show the plan without starting a bill')
4965
.action((opts) => {
5066
if (opts.kind === 'gpu' && !opts.maxHourlyPrice) {

0 commit comments

Comments
 (0)