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
27 changes: 27 additions & 0 deletions packages/cli/src/commands/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { parsePositiveFiniteNumber, parsePositiveSafeInteger } from './deploy.js';

describe('deploy numeric option parsers', () => {
it('accepts positive safe integer resource counts', () => {
expect(parsePositiveSafeInteger('4')).toBe(4);
});

it.each(['nope', '0', '-1', '1.5', 'Infinity', '9007199254740992'])(
'rejects invalid resource count %s',
(value) => {
expect(() => parsePositiveSafeInteger(value)).toThrow('positive safe integer');
},
);

it('accepts positive finite memory and price values', () => {
expect(parsePositiveFiniteNumber('0.5')).toBe(0.5);
expect(parsePositiveFiniteNumber('12.75')).toBe(12.75);
});

it.each(['nope', '0', '-1', 'NaN', 'Infinity', '-Infinity'])(
'rejects invalid finite value %s',
(value) => {
expect(() => parsePositiveFiniteNumber(value)).toThrow('positive finite number');
},
);
});
32 changes: 24 additions & 8 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { Command } from 'commander';
import { Command, InvalidArgumentError } from 'commander';
import kleur from 'kleur';

export function parsePositiveSafeInteger(value: string): number {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
Comment on lines +5 to +6

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 Hex and scientific-notation strings silently accepted

Number('0x10')16 and Number('1e2')100 both satisfy the safe-integer and finite-positive checks, so --cpu 0x10 or --gpu-count 1e2 pass validation and are forwarded to the action. A user who fat-fingers 0x or uses scientific notation by accident would get an unexpected count rather than a validation error. Adding an explicit check (/^\d+$/ for the integer parser, /^\d+(\.\d+)?$/ for the float parser) before calling Number() would close this gap without affecting normal decimal input.

throw new InvalidArgumentError('must be a positive safe integer');
}
return parsed;
}

export function parsePositiveFiniteNumber(value: string): number {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new InvalidArgumentError('must be a positive finite number');
}
return parsed;
}

export const deployCmd = new Command('deploy')
.description('Provision cloud infrastructure — VPS, GPU, bare metal, managed databases, object storage')
.action(() => {
Expand All @@ -21,10 +37,10 @@ deployCmd
.command('quote')
.description('Price-check a spec across every connected provider before provisioning')
.requiredOption('--kind <kind>', 'cpu-vps | gpu | bare-metal | managed-db | block-storage | object-storage')
.option('--cpu <n>', 'vCPU count', Number)
.option('--memory <gb>', 'RAM in GB', Number)
.option('--cpu <n>', 'vCPU count', parsePositiveSafeInteger)
.option('--memory <gb>', 'RAM in GB', parsePositiveFiniteNumber)
.option('--gpu <model>', 'GPU model, e.g. A100, H100, RTX-4090')
.option('--gpu-count <n>', 'GPUs per instance', Number)
.option('--gpu-count <n>', 'GPUs per instance', parsePositiveSafeInteger)
.option('--region <id>')
.option('--spot', 'accept interruptible / spot instances for lower price')
.action((opts) => {
Expand All @@ -37,14 +53,14 @@ deployCmd
.description('Spin up a new instance (WILL start billing — pair with a --max-hourly-price guardrail)')
.requiredOption('--provider <id>', 'e.g. cloud-runpod, cloud-digitalocean')
.requiredOption('--kind <kind>')
.option('--cpu <n>', 'vCPU count', Number)
.option('--memory <gb>', 'memory in GB', Number)
.option('--cpu <n>', 'vCPU count', parsePositiveSafeInteger)
.option('--memory <gb>', 'memory in GB', parsePositiveFiniteNumber)
.option('--gpu <model>', 'GPU model, e.g. A100, H100')
.option('--gpu-count <n>', 'number of GPUs', Number)
.option('--gpu-count <n>', 'number of GPUs', parsePositiveSafeInteger)
.option('--region <id>')
.option('--image <name>')
.option('--spot')
.option('--max-hourly-price <usd>', 'abort if quote exceeds this (strongly recommended for GPU)', Number)
.option('--max-hourly-price <usd>', 'abort if quote exceeds this (strongly recommended for GPU)', parsePositiveFiniteNumber)
.option('--dry-run', 'show the plan without starting a bill')
.action((opts) => {
if (opts.kind === 'gpu' && !opts.maxHourlyPrice) {
Expand Down
Loading