diff --git a/src/lib/duration-flags.test.ts b/src/lib/duration-flags.test.ts new file mode 100644 index 00000000000..ff05916e0bd --- /dev/null +++ b/src/lib/duration-flags.test.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { parseLogsSinceDuration, parseShieldsTimeoutDuration } from "./duration-flags"; + +describe("oclif duration flag parsers", () => { + it("normalizes logs --since durations", () => { + expect(parseLogsSinceDuration(" 5m ")).toBe("5m"); + expect(parseLogsSinceDuration("30s")).toBe("30s"); + }); + + it("rejects invalid logs --since durations with the public parser message", () => { + expect(() => parseLogsSinceDuration("0s")).toThrow( + "--since requires a positive duration like 5m, 1h, or 30s", + ); + expect(() => parseLogsSinceDuration("someday")).toThrow( + "--since requires a positive duration like 5m, 1h, or 30s", + ); + }); + + it("uses the shields duration parser for bounded shields timeouts", () => { + expect(parseShieldsTimeoutDuration(" 5m ")).toBe("5m"); + expect(() => parseShieldsTimeoutDuration("2h")).toThrow(/exceeds maximum/); + }); +}); diff --git a/src/lib/duration-flags.ts b/src/lib/duration-flags.ts new file mode 100644 index 00000000000..d5a391313e4 --- /dev/null +++ b/src/lib/duration-flags.ts @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Errors, Flags } from "@oclif/core"; + +import { parseDuration } from "./duration"; + +const LOGS_SINCE_DURATION_RE = /^[1-9]\d*(?:ms|s|m|h|d)$/i; + +export function parseLogsSinceDuration(input: string): string { + const trimmed = input.trim(); + if (!LOGS_SINCE_DURATION_RE.test(trimmed)) { + throw new Errors.CLIError("--since requires a positive duration like 5m, 1h, or 30s"); + } + return trimmed; +} + +export function parseShieldsTimeoutDuration(input: string): string { + const trimmed = input.trim(); + try { + parseDuration(trimmed); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Errors.CLIError(message); + } + return trimmed; +} + +export const logsSinceDurationFlag = Flags.custom({ + parse: async (input) => parseLogsSinceDuration(input), +}); + +export const shieldsTimeoutDurationFlag = Flags.custom({ + parse: async (input) => parseShieldsTimeoutDuration(input), +}); diff --git a/src/lib/sandbox-logs-cli-command.ts b/src/lib/sandbox-logs-cli-command.ts index 47e399d3b90..e20312dbf71 100644 --- a/src/lib/sandbox-logs-cli-command.ts +++ b/src/lib/sandbox-logs-cli-command.ts @@ -5,6 +5,7 @@ import { Args, Command, Flags } from "@oclif/core"; +import { logsSinceDurationFlag } from "./duration-flags"; import type { SandboxLogsOptions } from "./sandbox-logs-options"; import { DEFAULT_SANDBOX_LOG_LINES } from "./sandbox-logs-options"; import { showSandboxLogs } from "./sandbox-runtime-actions"; @@ -13,7 +14,6 @@ type SandboxLogsRuntimeBridge = { sandboxLogs: (sandboxName: string, options: SandboxLogsOptions) => void; }; -const LOGS_SINCE_DURATION_RE = /^[1-9]\d*(?:ms|s|m|h|d)$/i; const DEFAULT_SANDBOX_LOG_LINE_COUNT = Number(DEFAULT_SANDBOX_LOG_LINES); let runtimeBridgeFactory = (): SandboxLogsRuntimeBridge => ({ sandboxLogs: showSandboxLogs }); @@ -56,28 +56,17 @@ export default class SandboxLogsCommand extends Command { description: "Number of log lines to return", min: 1, }), - since: Flags.string({ + since: logsSinceDurationFlag({ description: "Only show logs from this duration ago, such as 5m, 1h, or 30s", }), }; - private normalizeSinceDuration(since: string | undefined): string | null { - if (since === undefined) { - return null; - } - const trimmed = since.trim(); - if (!LOGS_SINCE_DURATION_RE.test(trimmed)) { - this.error("--since requires a positive duration like 5m, 1h, or 30s", { exit: 2 }); - } - return trimmed; - } - public async run(): Promise { const { args, flags } = await this.parse(SandboxLogsCommand); getRuntimeBridge().sandboxLogs(args.sandboxName, { follow: flags.follow === true, lines: String(flags.tail), - since: this.normalizeSinceDuration(flags.since), + since: flags.since ?? null, }); } } diff --git a/src/lib/shields-cli-commands.ts b/src/lib/shields-cli-commands.ts index 7811f69f66a..608b0fbbb31 100644 --- a/src/lib/shields-cli-commands.ts +++ b/src/lib/shields-cli-commands.ts @@ -5,6 +5,7 @@ import { Args, Command, Flags } from "@oclif/core"; +import { shieldsTimeoutDurationFlag } from "./duration-flags"; import * as shields from "./shields"; const sandboxNameArg = Args.string({ @@ -23,7 +24,7 @@ export class ShieldsDownCommand extends Command { static args = { sandboxName: sandboxNameArg }; static flags = { help: Flags.help({ char: "h" }), - timeout: Flags.string({ description: "Duration before shields are restored" }), + timeout: shieldsTimeoutDurationFlag({ description: "Duration before shields are restored" }), reason: Flags.string({ description: "Reason for lowering shields" }), policy: Flags.string({ description: "Policy to apply while shields are down" }), };