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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ probitas list [options]
# Initialize configuration file
probitas init

# Cache scenario dependencies
probitas cache [options]

# Format scenario files
probitas fmt

Expand Down
30 changes: 30 additions & 0 deletions assets/usage-cache.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
probitas cache - Cache scenario dependencies

Usage: probitas cache [options] [paths...]

Arguments:
[paths...] Scenario files or directories
Defaults to current directory

Options:
-h, --help Show help message
-r, --reload Force re-download of cached dependencies
--include <pattern> Include pattern for file discovery
--exclude <pattern> Exclude pattern for file discovery
--config <path> Path to probitas config file
-v, --verbose Verbose output
-q, --quiet Suppress output
-d, --debug Debug output

Note:
Runs `deno cache` on discovered scenario files to pre-download dependencies.
Uses includes/excludes from probitas config (same as run/list).

Examples:
probitas cache # Cache dependencies for all scenarios
probitas cache api/ # Cache dependencies in api directory
probitas cache -r # Force re-download all dependencies
probitas cache -r api/ # Force re-download for api directory
probitas cache --include "e2e/**/*.probitas.ts"

Documentation: https://probitas-test.github.io/documents/
4 changes: 2 additions & 2 deletions assets/usage-list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ Options:
--no-env Skip loading .env file
-r, --reload Reload dependencies before running
--deno-<option>[=<value>] Pass options to deno subprocess
Value options: --deno-config=custom.json, --deno-lock=custom.lock
Boolean flags: --deno-no-lock, --deno-no-prompt
With value: --deno-config=custom.json, --deno-reload=jsr:@std/http
Without value: --deno-reload, --deno-no-lock, --deno-no-prompt

Selector Format:
[!][type:]value
Expand Down
4 changes: 2 additions & 2 deletions assets/usage-run.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ Options:
-d, --debug Debug output (maximum detail)
--no-color Disable colored output
--deno-<option>[=<value>] Pass options to deno subprocess
Value options: --deno-config=custom.json, --deno-lock=custom.lock
Boolean flags: --deno-no-lock, --deno-no-prompt
With value: --deno-config=custom.json, --deno-reload=jsr:@std/http
Without value: --deno-reload, --deno-no-lock, --deno-no-prompt

Selector Format:
[!][type:]value
Expand Down
1 change: 1 addition & 0 deletions assets/usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Commands:
init Initialize a new probitas project
run Run scenarios
list List available scenarios
cache Cache scenario dependencies (runs deno cache)
fmt Format scenario files (runs deno fmt)
lint Lint scenario files (runs deno lint)
check Type-check scenario files (runs deno check)
Expand Down
4 changes: 4 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { parseArgs } from "@std/cli";
import { getLogger } from "@logtape/logtape";
import { EXIT_CODE } from "./cli/constants.ts";
import { cacheCommand } from "./cli/commands/cache.ts";
import { checkCommand } from "./cli/commands/check.ts";
import { fmtCommand } from "./cli/commands/fmt.ts";
import { initCommand } from "./cli/commands/init.ts";
Expand Down Expand Up @@ -81,6 +82,9 @@ export async function main(args: string[]): Promise<number> {
case "list":
return await listCommand(commandArgs, cwd);

case "cache":
return await cacheCommand(commandArgs, cwd);

case "fmt":
return await fmtCommand(commandArgs, cwd);

Expand Down
12 changes: 9 additions & 3 deletions src/cli/commands/_deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface DenoSubcommandOptions {
usageAsset: string;
/** Extra arguments to pass to the deno command */
extraArgs?: readonly string[];
/** Enable -r/--reload option support */
supportReload?: boolean;
/** Use deno.json/deno.lock config (default: false, adds --no-config) */
useConfig?: boolean;
}

/**
Expand All @@ -46,13 +50,14 @@ export async function runDenoSubcommand(
try {
const parsed = parseArgs(args, {
string: ["config", "include", "exclude"],
boolean: ["help", "quiet", "verbose", "debug"],
boolean: ["help", "quiet", "verbose", "debug", "reload"],
collect: ["include", "exclude"],
alias: {
h: "help",
v: "verbose",
q: "quiet",
d: "debug",
r: "reload",
},
default: {
include: undefined,
Expand Down Expand Up @@ -108,10 +113,11 @@ export async function runDenoSubcommand(
fileCount: scenarioFiles.length,
});

// Run deno command with --no-config
// Run deno command (with --no-config unless useConfig is true)
const denoArgs = [
subcommand,
"--no-config",
...(options.useConfig ? [] : ["--no-config"]),
...(options.supportReload && parsed.reload ? ["--reload"] : []),
...(options.extraArgs ?? []),
...scenarioFiles,
];
Expand Down
27 changes: 27 additions & 0 deletions src/cli/commands/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Cache command for Probitas CLI
*
* Runs `deno cache` on discovered scenario files to pre-download dependencies.
*
* @module
*/

import { runDenoSubcommand } from "./_deno.ts";

/**
* Run the cache command
*
* @param args - Command-line arguments
* @param cwd - Current working directory
* @returns Exit code
*/
export async function cacheCommand(
args: string[],
cwd: string,
): Promise<number> {
return await runDenoSubcommand("cache", args, cwd, {
usageAsset: "usage-cache.txt",
supportReload: true,
useConfig: true,
});
}
13 changes: 11 additions & 2 deletions src/cli/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,20 @@ export async function listCommand(
// Configure logging with determined log level
try {
await configureLogging(logLevel);
logger.debug("List command started", { args, cwd, logLevel, denoArgs });
} catch {
// Silently ignore logging configuration errors (e.g., in test environments)
}

// Add --reload to denoArgs if -r/--reload is specified
const finalDenoArgs = parsed.reload ? [...denoArgs, "--reload"] : denoArgs;
Comment on lines +109 to +110
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user provides both -r and --deno-reload, this will result in duplicate --reload flags being passed to the Deno subprocess. Consider checking if --reload is already present in denoArgs before adding it.

Suggested change
// Add --reload to denoArgs if -r/--reload is specified
const finalDenoArgs = parsed.reload ? [...denoArgs, "--reload"] : denoArgs;
// Add --reload to denoArgs if -r/--reload is specified, avoiding duplicates
const finalDenoArgs = parsed.reload && !denoArgs.includes("--reload")
? [...denoArgs, "--reload"]
: denoArgs;

Copilot uses AI. Check for mistakes.

logger.debug("List command started", {
args,
cwd,
logLevel,
denoArgs: finalDenoArgs,
});

// Load environment variables before loading configuration
// This allows config files to reference environment variables
await loadEnvironment(cwd, {
Expand Down Expand Up @@ -172,7 +181,7 @@ export async function listCommand(
const { allScenarios, filteredScenarios } = await runListSubprocess(
scenarioFiles,
selectors,
denoArgs,
finalDenoArgs,
cwd,
);

Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* @module
*/

export { cacheCommand } from "./cache.ts";
export { checkCommand } from "./check.ts";
export { fmtCommand } from "./fmt.ts";
export { initCommand } from "./init.ts";
Expand Down
13 changes: 11 additions & 2 deletions src/cli/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,16 @@ export async function runCommand(

// Configure logging with determined log level
await configureLogging(logLevel);
logger.debug("Run command started", { args, cwd, logLevel, denoArgs });

// Add --reload to denoArgs if -r/--reload is specified
const finalDenoArgs = parsed.reload ? [...denoArgs, "--reload"] : denoArgs;
Comment on lines +129 to +130
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user provides both -r and --deno-reload, this will result in duplicate --reload flags being passed to the Deno subprocess. Consider checking if --reload is already present in denoArgs before adding it.

Suggested change
// Add --reload to denoArgs if -r/--reload is specified
const finalDenoArgs = parsed.reload ? [...denoArgs, "--reload"] : denoArgs;
// Add --reload to denoArgs if -r/--reload is specified, avoiding duplicates
const finalDenoArgs = parsed.reload && !denoArgs.includes("--reload")
? [...denoArgs, "--reload"]
: denoArgs;

Copilot uses AI. Check for mistakes.

logger.debug("Run command started", {
args,
cwd,
logLevel,
denoArgs: finalDenoArgs,
});

// Load environment variables before loading configuration
// This allows config files to reference environment variables
Expand Down Expand Up @@ -212,7 +221,7 @@ export async function runCommand(
timeout,
stepOptions,
logLevel,
denoArgs,
denoArgs: finalDenoArgs,
cwd,
signal,
});
Expand Down
21 changes: 5 additions & 16 deletions src/cli/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export interface ExtractDenoOptionsResult {
* Options starting with --deno- are extracted and converted to
* the corresponding deno option (e.g., --deno-config becomes --config).
*
* Value-taking options must use `=` syntax (e.g., `--deno-config=path`).
* Boolean flags work without values (e.g., `--deno-no-lock`).
* Options with `=` are passed as value options (e.g., `--deno-config=path` → `--config=path`).
* Options without `=` are passed as boolean flags (e.g., `--deno-reload` → `--reload`).
*
* @param args - Command-line arguments
* @returns Extracted deno args and remaining args
Expand All @@ -35,8 +35,9 @@ export interface ExtractDenoOptionsResult {
* "--selector", "foo",
* "--deno-lock=custom/deno.lock",
* "--deno-no-prompt",
* "--deno-reload",
* ]);
* // denoArgs: ["--config=custom/deno.json", "--lock=custom/deno.lock", "--no-prompt"]
* // denoArgs: ["--config=custom/deno.json", "--lock=custom/deno.lock", "--no-prompt", "--reload"]
* // remainingArgs: ["--selector", "foo"]
* ```
*/
Expand All @@ -46,20 +47,8 @@ export function extractDenoOptions(args: string[]): ExtractDenoOptionsResult {

for (const arg of args) {
if (arg.startsWith("--deno-")) {
const optionBody = arg.slice(7); // e.g., "config=path" or "no-prompt"
const hasValue = optionBody.includes("=");
const name = hasValue ? optionBody.split("=")[0] : optionBody;

// Value-taking options must use `=` syntax (e.g., --deno-config=path).
// Boolean flags follow the Deno `no-*` convention (e.g., --deno-no-lock).
if (!hasValue && !name.startsWith("no-")) {
throw new Error(
`Deno option "--deno-${name}" requires a value. ` +
`Use "--deno-${name}=<value>" syntax.`,
);
}

// Convert --deno-xxx to --xxx (preserving =value if present)
const optionBody = arg.slice(7); // e.g., "config=path" or "reload"
const denoArg = "--" + optionBody;
denoArgs.push(denoArg);
} else {
Expand Down
79 changes: 79 additions & 0 deletions src/cli/utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@std/assert";
import { describe, it } from "@std/testing/bdd";
import {
extractDenoOptions,
getVersion,
parsePositiveInteger,
parseTimeout,
Expand All @@ -21,6 +22,84 @@ import {
import { JSONReporter, ListReporter } from "@probitas/reporter";

describe("utils", () => {
describe("extractDenoOptions", () => {
it("extracts --deno-* options with values", () => {
const result = extractDenoOptions([
"--deno-config=custom/deno.json",
"--selector",
"foo",
"--deno-lock=custom/deno.lock",
]);
assertEquals(result.denoArgs, [
"--config=custom/deno.json",
"--lock=custom/deno.lock",
]);
assertEquals(result.remainingArgs, ["--selector", "foo"]);
});

it("extracts --deno-no-* boolean flags", () => {
const result = extractDenoOptions([
"--deno-no-prompt",
"--deno-no-lock",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--no-prompt", "--no-lock"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("extracts --deno-reload without value as boolean flag", () => {
const result = extractDenoOptions([
"--deno-reload",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--reload"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("extracts --deno-reload with value", () => {
const result = extractDenoOptions([
"--deno-reload=jsr:@std/http",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--reload=jsr:@std/http"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("extracts --deno-check without value as boolean flag", () => {
const result = extractDenoOptions([
"--deno-check",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--check"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("extracts --deno-check with value", () => {
const result = extractDenoOptions([
"--deno-check=all",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--check=all"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("extracts any --deno-* option without value as boolean flag", () => {
const result = extractDenoOptions([
"--deno-config",
"--deno-lock",
"scenarios/",
]);
assertEquals(result.denoArgs, ["--config", "--lock"]);
assertEquals(result.remainingArgs, ["scenarios/"]);
});

it("returns empty arrays when no deno options", () => {
const result = extractDenoOptions(["--selector", "foo"]);
assertEquals(result.denoArgs, []);
assertEquals(result.remainingArgs, ["--selector", "foo"]);
});
});

describe("resolveReporter", () => {
const reporters = [
{ name: "list", class: ListReporter },
Expand Down
Loading