From 87f9693dff4ad204d3a0f1230a558aabc4de9d33 Mon Sep 17 00:00:00 2001 From: lazyGPT07 Date: Sat, 13 Jun 2026 15:51:04 -0600 Subject: [PATCH] fix(cli): validate positive numeric options --- packages/cli/src/index.ts | 17 +++++++++-------- packages/cli/src/numeric-options.test.ts | 19 +++++++++++++++++++ packages/cli/src/numeric-options.ts | 9 +++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 packages/cli/src/numeric-options.test.ts create mode 100644 packages/cli/src/numeric-options.ts diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fb169d6..59f7dd8 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -10,6 +10,7 @@ import { assertSchemaKind, parseDocument, validate } from "@logicsrc/validators" import { getConfigValue, readConfig, setConfigValue, writeConfig } from "./config.js"; import { boards, tasks } from "./fixtures.js"; import { print, type OutputFormat } from "./format.js"; +import { parsePositiveInteger } from "./numeric-options.js"; import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./openspec.js"; import { defaultPluginRegistry } from "./registry.js"; @@ -88,7 +89,7 @@ program program .command("read") .argument("", "Board path") - .option("--limit ", "Number of posts", "20") + .option("--limit ", "Number of posts", parsePositiveInteger, 20) .option("--format ", "table, json, or markdown", "table") .description("Read a board feed.") .action((board, options) => { @@ -97,7 +98,7 @@ program { type: "TASK", board, title: "QA checkout flow", meta: "25 USDC" }, { type: "POST", board, title: "New agent plugin idea", meta: "4 replies" }, { type: "RUN", board, title: "qa-agent completed task_123", meta: "completed" } - ].slice(0, Number(options.limit)), + ].slice(0, options.limit), options.format as OutputFormat ); }); @@ -306,8 +307,8 @@ feeds .argument("", "Keyword, phrase, or homepage URL") .option("--type ", "Feed kind or all", "all") .option("--format ", "json, opml, rss, atom, or json-feed", "json") - .option("--limit ", "Maximum results", "25") - .option("--freshness-days ", "Freshness window for callers that need it") + .option("--limit ", "Maximum results", parsePositiveInteger, 25) + .option("--freshness-days ", "Freshness window for callers that need it", parsePositiveInteger) .option("--include-dead-feeds", "Include feeds that fail validation") .option("--include-unvalidated", "Return provider candidates without validation") .option("--providers ", "Comma-separated provider ids") @@ -316,8 +317,8 @@ feeds const response = await discoverFeeds({ q: keyword, type: options.type as FeedKind | "all", - limit: Number(options.limit), - freshnessDays: options.freshnessDays ? Number(options.freshnessDays) : undefined, + limit: options.limit, + freshnessDays: options.freshnessDays, includeDeadFeeds: Boolean(options.includeDeadFeeds), includeUnvalidated: Boolean(options.includeUnvalidated), providers: splitOption(options.providers) @@ -362,10 +363,10 @@ feeds feeds .command("export-opml") .argument("", "Keyword or phrase") - .option("--limit ", "Maximum results", "100") + .option("--limit ", "Maximum results", parsePositiveInteger, 100) .description("Discover feeds and print OPML.") .action(async (keyword, options) => { - const response = await discoverFeeds({ q: keyword, limit: Number(options.limit) }); + const response = await discoverFeeds({ q: keyword, limit: options.limit }); console.log(renderDiscoveryOutput(response, "opml")); }); diff --git a/packages/cli/src/numeric-options.test.ts b/packages/cli/src/numeric-options.test.ts new file mode 100644 index 0000000..c3c0519 --- /dev/null +++ b/packages/cli/src/numeric-options.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { parsePositiveInteger } from "./numeric-options.js"; + +describe("parsePositiveInteger", () => { + it.each([ + ["1", 1], + ["25", 25], + ["100", 100] + ])("parses %s", (value, expected) => { + expect(parsePositiveInteger(value)).toBe(expected); + }); + + it.each(["", " ", "nope", "0", "-1", "1.5", "Infinity", "NaN"])( + "rejects invalid input: %s", + (value) => { + expect(() => parsePositiveInteger(value)).toThrow("must be a positive integer"); + } + ); +}); diff --git a/packages/cli/src/numeric-options.ts b/packages/cli/src/numeric-options.ts new file mode 100644 index 0000000..1c49ed7 --- /dev/null +++ b/packages/cli/src/numeric-options.ts @@ -0,0 +1,9 @@ +import { InvalidArgumentError } from "commander"; + +export function parsePositiveInteger(value: string): number { + const parsed = Number(value); + if (value.trim() === "" || !Number.isSafeInteger(parsed) || parsed < 1) { + throw new InvalidArgumentError("must be a positive integer"); + } + return parsed; +}