Skip to content

Commit fbf4d76

Browse files
authored
fix(cli): validate positive numeric options (#49)
1 parent aa3fdba commit fbf4d76

3 files changed

Lines changed: 37 additions & 8 deletions

File tree

packages/cli/src/index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { assertSchemaKind, parseDocument, validate } from "@logicsrc/validators"
1010
import { getConfigValue, readConfig, setConfigValue, writeConfig } from "./config.js";
1111
import { boards, tasks } from "./fixtures.js";
1212
import { print, type OutputFormat } from "./format.js";
13+
import { parsePositiveInteger } from "./numeric-options.js";
1314
import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./openspec.js";
1415
import { defaultPluginRegistry } from "./registry.js";
1516

@@ -88,7 +89,7 @@ program
8889
program
8990
.command("read")
9091
.argument("<board>", "Board path")
91-
.option("--limit <limit>", "Number of posts", "20")
92+
.option("--limit <limit>", "Number of posts", parsePositiveInteger, 20)
9293
.option("--format <format>", "table, json, or markdown", "table")
9394
.description("Read a board feed.")
9495
.action((board, options) => {
@@ -97,7 +98,7 @@ program
9798
{ type: "TASK", board, title: "QA checkout flow", meta: "25 USDC" },
9899
{ type: "POST", board, title: "New agent plugin idea", meta: "4 replies" },
99100
{ type: "RUN", board, title: "qa-agent completed task_123", meta: "completed" }
100-
].slice(0, Number(options.limit)),
101+
].slice(0, options.limit),
101102
options.format as OutputFormat
102103
);
103104
});
@@ -306,8 +307,8 @@ feeds
306307
.argument("<keyword>", "Keyword, phrase, or homepage URL")
307308
.option("--type <type>", "Feed kind or all", "all")
308309
.option("--format <format>", "json, opml, rss, atom, or json-feed", "json")
309-
.option("--limit <limit>", "Maximum results", "25")
310-
.option("--freshness-days <days>", "Freshness window for callers that need it")
310+
.option("--limit <limit>", "Maximum results", parsePositiveInteger, 25)
311+
.option("--freshness-days <days>", "Freshness window for callers that need it", parsePositiveInteger)
311312
.option("--include-dead-feeds", "Include feeds that fail validation")
312313
.option("--include-unvalidated", "Return provider candidates without validation")
313314
.option("--providers <providers>", "Comma-separated provider ids")
@@ -316,8 +317,8 @@ feeds
316317
const response = await discoverFeeds({
317318
q: keyword,
318319
type: options.type as FeedKind | "all",
319-
limit: Number(options.limit),
320-
freshnessDays: options.freshnessDays ? Number(options.freshnessDays) : undefined,
320+
limit: options.limit,
321+
freshnessDays: options.freshnessDays,
321322
includeDeadFeeds: Boolean(options.includeDeadFeeds),
322323
includeUnvalidated: Boolean(options.includeUnvalidated),
323324
providers: splitOption(options.providers)
@@ -362,10 +363,10 @@ feeds
362363
feeds
363364
.command("export-opml")
364365
.argument("<keyword>", "Keyword or phrase")
365-
.option("--limit <limit>", "Maximum results", "100")
366+
.option("--limit <limit>", "Maximum results", parsePositiveInteger, 100)
366367
.description("Discover feeds and print OPML.")
367368
.action(async (keyword, options) => {
368-
const response = await discoverFeeds({ q: keyword, limit: Number(options.limit) });
369+
const response = await discoverFeeds({ q: keyword, limit: options.limit });
369370
console.log(renderDiscoveryOutput(response, "opml"));
370371
});
371372

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parsePositiveInteger } from "./numeric-options.js";
3+
4+
describe("parsePositiveInteger", () => {
5+
it.each([
6+
["1", 1],
7+
["25", 25],
8+
["100", 100]
9+
])("parses %s", (value, expected) => {
10+
expect(parsePositiveInteger(value)).toBe(expected);
11+
});
12+
13+
it.each(["", " ", "nope", "0", "-1", "1.5", "Infinity", "NaN"])(
14+
"rejects invalid input: %s",
15+
(value) => {
16+
expect(() => parsePositiveInteger(value)).toThrow("must be a positive integer");
17+
}
18+
);
19+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { InvalidArgumentError } from "commander";
2+
3+
export function parsePositiveInteger(value: string): number {
4+
const parsed = Number(value);
5+
if (value.trim() === "" || !Number.isSafeInteger(parsed) || parsed < 1) {
6+
throw new InvalidArgumentError("must be a positive integer");
7+
}
8+
return parsed;
9+
}

0 commit comments

Comments
 (0)