From 0b94335e0eb4694e475d3a89d3cfce7d441a957d Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:15:03 +0200 Subject: [PATCH 01/12] set up config, add api key --- .gitignore | 1 + package.json | 4 +-- src/api.ts | 78 ++++++++++++++++++++++++------------------- src/cli.ts | 76 ++++++++++++++++++++++++++++++++++------- src/config.ts | 37 ++++++++++++++++++++ src/consoleActions.ts | 13 ++++++++ 6 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 src/config.ts create mode 100644 src/consoleActions.ts diff --git a/.gitignore b/.gitignore index 1fb2798..234c552 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ src/version.ts node_modules/ .idea +octomind.config.json diff --git a/package.json b/package.json index be5e19b..ae4e176 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "lint": "npx genversion -des src/version.ts && eslint src/**/*.ts tests/**/*.ts --max-warnings=0", "build": "npx genversion -des src/version.ts && tsc --project tsconfig.build.json", - "octomind": "tsx src/cli.ts", + "octomind": "tsx src/index.ts", "test": "npx genversion -des src/version.ts && jest", "test:watch": "npx genversion -e src/version.ts && jest --watch" }, @@ -26,8 +26,8 @@ "tsx": "^4.19.3" }, "devDependencies": { - "@types/node": "^24.0.1", "@types/jest": "^30.0.0", + "@types/node": "^24.0.1", "@typescript-eslint/parser": "^8.25.0", "eslint": "8.57.1", "eslint-config-prettier": "^10.0.1", diff --git a/src/api.ts b/src/api.ts index 4e326fb..e720471 100644 --- a/src/api.ts +++ b/src/api.ts @@ -18,6 +18,7 @@ import { Environment, TestReport, } from "./types"; +import { loadConfig } from "./config"; const BASE_URL = "https://app.octomind.dev/api"; @@ -26,7 +27,7 @@ const apiCall = async ( method: "get" | "post" | "put" | "delete" | "patch", endpoint: string, apiKey: string, - data?: unknown, + data?: unknown ): Promise => { try { const response = await axios({ @@ -54,9 +55,10 @@ const outputResult = (result: unknown): void => { }; export const executeTests = async ( - options: ExecuteTestsOptions, + options: ExecuteTestsOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -80,8 +82,8 @@ export const executeTests = async ( const response = await apiCall( "post", "/apiKey/v2/execute", - options.apiKey, - requestBody, + config.apiKey, + requestBody ); if (options.json) { @@ -108,9 +110,10 @@ export const executeTests = async ( }; export const getTestReport = async ( - options: GetTestReportOptions, + options: GetTestReportOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -118,7 +121,7 @@ export const getTestReport = async ( const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/test-reports/${options.reportId}`, - options.apiKey, + config.apiKey ); if (options.json) { @@ -145,9 +148,10 @@ export const getTestReport = async ( }; export const registerLocation = async ( - options: RegisterLocationOptions, + options: RegisterLocationOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -164,8 +168,8 @@ export const registerLocation = async ( const response = await apiCall( "put", "/apiKey/v1/private-location/register", - options.apiKey, - requestBody, + config.apiKey, + requestBody ); if (options.json) { @@ -177,9 +181,10 @@ export const registerLocation = async ( }; export const unregisterLocation = async ( - options: UnregisterLocationOptions, + options: UnregisterLocationOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -191,8 +196,8 @@ export const unregisterLocation = async ( const response = await apiCall( "put", "/apiKey/v1/private-location/unregister", - options.apiKey, - requestBody, + config.apiKey, + requestBody ); if (options.json) { @@ -202,14 +207,15 @@ export const unregisterLocation = async ( console.log( "Unregistration result:", - response.success ? "Success" : "Failed", + response.success ? "Success" : "Failed" ); }; export const listPrivateLocations = async ( - options: ListPrivateLocationsOptions, + options: ListPrivateLocationsOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -217,7 +223,7 @@ export const listPrivateLocations = async ( const response = await apiCall( "get", "/apiKey/v1/private-location", - options.apiKey, + config.apiKey ); if (options.json) { @@ -234,9 +240,10 @@ export const listPrivateLocations = async ( }; export const listEnvironments = async ( - options: ListEnvironmentsOptions, + options: ListEnvironmentsOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -244,7 +251,7 @@ export const listEnvironments = async ( const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, - options.apiKey, + config.apiKey ); if (options.json) { @@ -262,9 +269,10 @@ export const listEnvironments = async ( }; export const createEnvironment = async ( - options: CreateEnvironmentOptions, + options: CreateEnvironmentOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -281,8 +289,8 @@ export const createEnvironment = async ( const response = await apiCall( "post", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, - options.apiKey, - requestBody, + config.apiKey, + requestBody ); if (options.json) { @@ -298,9 +306,10 @@ export const createEnvironment = async ( }; export const updateEnvironment = async ( - options: UpdateEnvironmentOptions, + options: UpdateEnvironmentOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -317,8 +326,8 @@ export const updateEnvironment = async ( const response = await apiCall( "patch", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, - options.apiKey, - requestBody, + config.apiKey, + requestBody ); if (options.json) { @@ -334,9 +343,10 @@ export const updateEnvironment = async ( }; export const deleteEnvironment = async ( - options: DeleteEnvironmentOptions, + options: DeleteEnvironmentOptions ): Promise => { - if (!options.apiKey) { + const config = await loadConfig(); + if (!config.apiKey) { console.error("API key is required"); process.exit(1); } @@ -344,7 +354,7 @@ export const deleteEnvironment = async ( await apiCall( "delete", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, - options.apiKey, + config.apiKey ); if (options.json) { diff --git a/src/cli.ts b/src/cli.ts index a8b93c8..7432250 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,18 +11,12 @@ import { unregisterLocation, updateEnvironment, } from "./api"; - -const apiKeyOption = new Option( - "-k, --api-key ", - "the api key for authentication", -) - .env("APIKEY") - .makeOptionMandatory(); +import { Config, loadConfig, saveConfig } from "./config"; +import { promptUser } from "./consoleActions"; const createCommandWithCommonOptions = (command: string): Command => { return program .command(command) - .addOption(apiKeyOption) .option("-j, --json", "Output raw JSON response"); }; @@ -33,10 +27,68 @@ export const buildCmd = (): Command => { program .name("octomind-cli") .description( - `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/`, + `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/` ) .version(version); + program + .command("init") + .description("Initialize configuration by setting up API key") + .option("-f, --force", "Force overwrite existing configuration") + .action(async (options: { force?: boolean }) => { + try { + console.log("šŸš€ Initializing configuration...\n"); + + const existingConfig = await loadConfig(); + + if (existingConfig.apiKey && !options.force) { + console.log("āš ļø Configuration already exists."); + const overwrite = await promptUser( + "Do you want to overwrite it? (y/N): " + ); + + if ( + overwrite.toLowerCase() !== "y" && + overwrite.toLowerCase() !== "yes" + ) { + console.log("Configuration unchanged."); + return; + } + } + + // Prompt for API key + const apiKey = await promptUser( + "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: " + ); + + if (!apiKey) { + console.log("āŒ API key is required."); + process.exit(1); + } + + // Optional: Prompt for additional configuration + const baseUrl = await promptUser( + "Enter base URL (optional, press Enter to skip): " + ); + + const newConfig: Config = { + ...existingConfig, + apiKey, + ...(baseUrl && { baseUrl }), + }; + + await saveConfig(newConfig); + + console.log("\n✨ Initialization complete!"); + } catch (error) { + console.error( + "āŒ Error during initialization:", + (error as Error).message + ); + process.exit(1); + } + }); + createCommandWithCommonOptions("execute") .description("Execute test cases") .requiredOption("-t, --test-target-id ", "Test target ID") @@ -47,7 +99,7 @@ export const buildCmd = (): Command => { .option( "-v, --variables-to-overwrite ", "JSON object of variables to overwrite", - toJSON, + toJSON ) .action(executeTests); @@ -88,7 +140,7 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key", + "Test account OTP initializer key" ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") @@ -106,7 +158,7 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key", + "Test account OTP initializer key" ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e5250a1 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,37 @@ +import path from "path"; +import fs from "fs/promises"; + +export interface Config { + apiKey?: string; + baseUrl?: string; + testTargetId?: string; +} + +const OCTOMIND_CONFIG_FILE = "octomind.config.json"; + +export function getConfigPath(): string { + return path.join(process.cwd(), OCTOMIND_CONFIG_FILE); +} + +export async function loadConfig(): Promise { + try { + const configPath = getConfigPath(); + const data = await fs.readFile(configPath, "utf8"); + return JSON.parse(data); + } catch (error) { + console.error("āŒ Error parsing configuration:", (error as Error).message); + + return {}; + } +} + +export async function saveConfig(config: Config): Promise { + try { + const configPath = getConfigPath(); + await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8"); + console.log(`āœ… Configuration saved to ${configPath}`); + } catch (error) { + console.error("āŒ Error saving configuration:", (error as Error).message); + process.exit(1); + } +} diff --git a/src/consoleActions.ts b/src/consoleActions.ts new file mode 100644 index 0000000..2d51de3 --- /dev/null +++ b/src/consoleActions.ts @@ -0,0 +1,13 @@ +import { createInterface } from "node:readline"; +import { stdin as input, stdout as output } from "node:process"; + +export function promptUser(question: string): Promise { + const rl = createInterface({ input, output }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} From 39a5ab575cb39ca6c99b284e8ddf2994edc3bbe7 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:24:28 +0200 Subject: [PATCH 02/12] lint --- src/api.ts | 40 ++++++++++++++++++++-------------------- src/cli.ts | 18 +++++++++--------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/api.ts b/src/api.ts index e720471..09c6201 100644 --- a/src/api.ts +++ b/src/api.ts @@ -27,7 +27,7 @@ const apiCall = async ( method: "get" | "post" | "put" | "delete" | "patch", endpoint: string, apiKey: string, - data?: unknown + data?: unknown, ): Promise => { try { const response = await axios({ @@ -55,7 +55,7 @@ const outputResult = (result: unknown): void => { }; export const executeTests = async ( - options: ExecuteTestsOptions + options: ExecuteTestsOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -83,7 +83,7 @@ export const executeTests = async ( "post", "/apiKey/v2/execute", config.apiKey, - requestBody + requestBody, ); if (options.json) { @@ -110,7 +110,7 @@ export const executeTests = async ( }; export const getTestReport = async ( - options: GetTestReportOptions + options: GetTestReportOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -121,7 +121,7 @@ export const getTestReport = async ( const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/test-reports/${options.reportId}`, - config.apiKey + config.apiKey, ); if (options.json) { @@ -148,7 +148,7 @@ export const getTestReport = async ( }; export const registerLocation = async ( - options: RegisterLocationOptions + options: RegisterLocationOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -169,7 +169,7 @@ export const registerLocation = async ( "put", "/apiKey/v1/private-location/register", config.apiKey, - requestBody + requestBody, ); if (options.json) { @@ -181,7 +181,7 @@ export const registerLocation = async ( }; export const unregisterLocation = async ( - options: UnregisterLocationOptions + options: UnregisterLocationOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -197,7 +197,7 @@ export const unregisterLocation = async ( "put", "/apiKey/v1/private-location/unregister", config.apiKey, - requestBody + requestBody, ); if (options.json) { @@ -207,12 +207,12 @@ export const unregisterLocation = async ( console.log( "Unregistration result:", - response.success ? "Success" : "Failed" + response.success ? "Success" : "Failed", ); }; export const listPrivateLocations = async ( - options: ListPrivateLocationsOptions + options: ListPrivateLocationsOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -223,7 +223,7 @@ export const listPrivateLocations = async ( const response = await apiCall( "get", "/apiKey/v1/private-location", - config.apiKey + config.apiKey, ); if (options.json) { @@ -240,7 +240,7 @@ export const listPrivateLocations = async ( }; export const listEnvironments = async ( - options: ListEnvironmentsOptions + options: ListEnvironmentsOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -251,7 +251,7 @@ export const listEnvironments = async ( const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, - config.apiKey + config.apiKey, ); if (options.json) { @@ -269,7 +269,7 @@ export const listEnvironments = async ( }; export const createEnvironment = async ( - options: CreateEnvironmentOptions + options: CreateEnvironmentOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -290,7 +290,7 @@ export const createEnvironment = async ( "post", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, config.apiKey, - requestBody + requestBody, ); if (options.json) { @@ -306,7 +306,7 @@ export const createEnvironment = async ( }; export const updateEnvironment = async ( - options: UpdateEnvironmentOptions + options: UpdateEnvironmentOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -327,7 +327,7 @@ export const updateEnvironment = async ( "patch", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, config.apiKey, - requestBody + requestBody, ); if (options.json) { @@ -343,7 +343,7 @@ export const updateEnvironment = async ( }; export const deleteEnvironment = async ( - options: DeleteEnvironmentOptions + options: DeleteEnvironmentOptions, ): Promise => { const config = await loadConfig(); if (!config.apiKey) { @@ -354,7 +354,7 @@ export const deleteEnvironment = async ( await apiCall( "delete", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, - config.apiKey + config.apiKey, ); if (options.json) { diff --git a/src/cli.ts b/src/cli.ts index 7432250..79f7cec 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,4 @@ -import { program, Option, Command } from "commander"; +import { program, Command } from "commander"; import { version } from "./version"; import { createEnvironment, @@ -27,7 +27,7 @@ export const buildCmd = (): Command => { program .name("octomind-cli") .description( - `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/` + `Octomind CLI tool. Version: ${version}. see https://octomind.dev/docs/api-reference/`, ) .version(version); @@ -44,7 +44,7 @@ export const buildCmd = (): Command => { if (existingConfig.apiKey && !options.force) { console.log("āš ļø Configuration already exists."); const overwrite = await promptUser( - "Do you want to overwrite it? (y/N): " + "Do you want to overwrite it? (y/N): ", ); if ( @@ -58,7 +58,7 @@ export const buildCmd = (): Command => { // Prompt for API key const apiKey = await promptUser( - "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: " + "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: ", ); if (!apiKey) { @@ -68,7 +68,7 @@ export const buildCmd = (): Command => { // Optional: Prompt for additional configuration const baseUrl = await promptUser( - "Enter base URL (optional, press Enter to skip): " + "Enter base URL (optional, press Enter to skip): ", ); const newConfig: Config = { @@ -83,7 +83,7 @@ export const buildCmd = (): Command => { } catch (error) { console.error( "āŒ Error during initialization:", - (error as Error).message + (error as Error).message, ); process.exit(1); } @@ -99,7 +99,7 @@ export const buildCmd = (): Command => { .option( "-v, --variables-to-overwrite ", "JSON object of variables to overwrite", - toJSON + toJSON, ) .action(executeTests); @@ -140,7 +140,7 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key" + "Test account OTP initializer key", ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") @@ -158,7 +158,7 @@ export const buildCmd = (): Command => { .option("--test-account-password ", "Test account password") .option( "--test-account-otp-initializer-key ", - "Test account OTP initializer key" + "Test account OTP initializer key", ) .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") From c920a44a755325378c7fa5a2b4b680367b892476 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:26:32 +0200 Subject: [PATCH 03/12] test --- tests/cli.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts index 31911d4..c801a14 100644 --- a/tests/cli.spec.ts +++ b/tests/cli.spec.ts @@ -9,8 +9,6 @@ describe("CLI Commands parsing options", () => { "node", "cli.js", "execute", - "--api-key", - "test-api-key", "--test-target-id", "test-target-id", "--url", From 3d7df063ca6f01c358537f71cc7f68f055a131cc Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:54:36 +0200 Subject: [PATCH 04/12] test target id in config --- src/cli.ts | 19 +++++++++---------- src/config.ts | 1 - tests/cli.spec.ts | 10 +--------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 79f7cec..86ca4ab 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -66,15 +66,14 @@ export const buildCmd = (): Command => { process.exit(1); } - // Optional: Prompt for additional configuration - const baseUrl = await promptUser( - "Enter base URL (optional, press Enter to skip): ", + const testTargetId = await promptUser( + "Enter test target id (optional, press Enter to skip): ", ); const newConfig: Config = { ...existingConfig, apiKey, - ...(baseUrl && { baseUrl }), + ...(testTargetId && { testTargetId }), }; await saveConfig(newConfig); @@ -91,8 +90,8 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("execute") .description("Execute test cases") - .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-u, --url ", "URL to test") + .option("-t, --test-target-id ", "Test target ID") .option("-e, --environment ", "Environment name", "default") .option("-d, --description ", "Test description") .option("-g, --tags ", "comma separated list of tags", splitter) @@ -105,8 +104,8 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("report") .description("Get test report details") - .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-r, --report-id ", "Test report ID") + .option("-t, --test-target-id ", "Test target ID") .action(getTestReport); createCommandWithCommonOptions("register-location") @@ -128,14 +127,14 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("list-environments") .description("List all environments") - .requiredOption("-t, --test-target-id ", "Test target ID") + .option("-t, --test-target-id ", "Test target ID") .action(listEnvironments); createCommandWithCommonOptions("create-environment") .description("Create a new environment") - .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-n, --name ", "Environment name") .requiredOption("-d, --discovery-url ", "Discovery URL") + .option("-t, --test-target-id ", "Test target ID") .option("--test-account-username ", "Test account username") .option("--test-account-password ", "Test account password") .option( @@ -150,8 +149,8 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("update-environment") .description("Update an existing environment") - .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-e, --environment-id ", "Environment ID") + .option("-t, --test-target-id ", "Test target ID") .option("-n, --name ", "Environment name") .option("-d, --discovery-url ", "Discovery URL") .option("--test-account-username ", "Test account username") @@ -168,8 +167,8 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("delete-environment") .description("Delete an environment") - .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-e, --environment-id ", "Environment ID") + .option("-t, --test-target-id ", "Test target ID") .action(deleteEnvironment); return program; }; diff --git a/src/config.ts b/src/config.ts index e5250a1..cace405 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,6 @@ import fs from "fs/promises"; export interface Config { apiKey?: string; - baseUrl?: string; testTargetId?: string; } diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts index c801a14..89e6271 100644 --- a/tests/cli.spec.ts +++ b/tests/cli.spec.ts @@ -5,15 +5,7 @@ import { executeTests } from "../src/api"; jest.mock("../src/api"); describe("CLI Commands parsing options", () => { - const stdArgs = [ - "node", - "cli.js", - "execute", - "--test-target-id", - "test-target-id", - "--url", - "https://example.com", - ]; + const stdArgs = ["node", "cli.js", "execute", "--url", "https://example.com"]; beforeAll(() => { buildCmd(); From 2529268a007569fd92724aa8a9ef4df7b4a73415 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:11:04 +0200 Subject: [PATCH 05/12] options for apiKey and testTargetId in init --- src/cli.ts | 103 +++++++++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 46 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 86ca4ab..5f1895e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -34,59 +34,70 @@ export const buildCmd = (): Command => { program .command("init") .description("Initialize configuration by setting up API key") + .option("-t, --test-target-id ", "Test target ID") + .option("-k, --api-key ", "the api key for authentication") .option("-f, --force", "Force overwrite existing configuration") - .action(async (options: { force?: boolean }) => { - try { - console.log("šŸš€ Initializing configuration...\n"); - - const existingConfig = await loadConfig(); - - if (existingConfig.apiKey && !options.force) { - console.log("āš ļø Configuration already exists."); - const overwrite = await promptUser( - "Do you want to overwrite it? (y/N): ", - ); + .action( + async (options: { + testTargetId?: string; + apiKey: string; + force?: boolean; + }) => { + try { + console.log("šŸš€ Initializing configuration...\n"); + + const existingConfig = await loadConfig(); + + if (existingConfig.apiKey && !options.force) { + console.log("āš ļø Configuration already exists."); + const overwrite = await promptUser( + "Do you want to overwrite it? (y/N): ", + ); + + if ( + overwrite.toLowerCase() !== "y" && + overwrite.toLowerCase() !== "yes" + ) { + console.log("Configuration unchanged."); + return; + } + } - if ( - overwrite.toLowerCase() !== "y" && - overwrite.toLowerCase() !== "yes" - ) { - console.log("Configuration unchanged."); - return; + let apiKey; + if (!options.apiKey) { + apiKey = await promptUser( + "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: ", + ); + if (!apiKey) { + console.log("āŒ API key is required."); + process.exit(1); + } } - } + let testTargetId; + if (!options.testTargetId) { + testTargetId = await promptUser( + "Enter test target id (optional, press Enter to skip): ", + ); + } + + const newConfig: Config = { + ...existingConfig, + apiKey: options.apiKey ?? apiKey, + testTargetId: options.testTargetId ?? testTargetId, + }; - // Prompt for API key - const apiKey = await promptUser( - "Enter your API key. Go to https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key to learn how to generate one: ", - ); + await saveConfig(newConfig); - if (!apiKey) { - console.log("āŒ API key is required."); + console.log("\n✨ Initialization complete!"); + } catch (error) { + console.error( + "āŒ Error during initialization:", + (error as Error).message, + ); process.exit(1); } - - const testTargetId = await promptUser( - "Enter test target id (optional, press Enter to skip): ", - ); - - const newConfig: Config = { - ...existingConfig, - apiKey, - ...(testTargetId && { testTargetId }), - }; - - await saveConfig(newConfig); - - console.log("\n✨ Initialization complete!"); - } catch (error) { - console.error( - "āŒ Error during initialization:", - (error as Error).message, - ); - process.exit(1); - } - }); + }, + ); createCommandWithCommonOptions("execute") .description("Execute test cases") From e6f7873bc5c7f261955c6f90d8016d80eaa33855 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:23:40 +0200 Subject: [PATCH 06/12] refactor --- src/api.ts | 82 ++++++++++++------------------------------------------ 1 file changed, 18 insertions(+), 64 deletions(-) diff --git a/src/api.ts b/src/api.ts index 09c6201..ee0cdc6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -29,13 +29,21 @@ const apiCall = async ( apiKey: string, data?: unknown, ): Promise => { + const config = await loadConfig(); + if (!apiKey && !config.apiKey) { + console.error( + "API key is required. Please set it in your config via init command or pass it as an option.", + ); + process.exit(1); + } + try { const response = await axios({ method, url: `${BASE_URL}${endpoint}`, data, headers: { - "X-API-Key": apiKey, + "X-API-Key": apiKey ?? config.apiKey, "Content-Type": "application/json", }, }); @@ -57,12 +65,6 @@ const outputResult = (result: unknown): void => { export const executeTests = async ( options: ExecuteTestsOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const requestBody: TestTargetExecutionRequest = { testTargetId: options.testTargetId, url: options.url, @@ -82,7 +84,7 @@ export const executeTests = async ( const response = await apiCall( "post", "/apiKey/v2/execute", - config.apiKey, + options.apiKey, requestBody, ); @@ -112,16 +114,10 @@ export const executeTests = async ( export const getTestReport = async ( options: GetTestReportOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/test-reports/${options.reportId}`, - config.apiKey, + options.apiKey, ); if (options.json) { @@ -150,12 +146,6 @@ export const getTestReport = async ( export const registerLocation = async ( options: RegisterLocationOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const requestBody: RegisterRequest = { name: options.name, registrationData: { @@ -168,7 +158,7 @@ export const registerLocation = async ( const response = await apiCall( "put", "/apiKey/v1/private-location/register", - config.apiKey, + options.apiKey, requestBody, ); @@ -183,12 +173,6 @@ export const registerLocation = async ( export const unregisterLocation = async ( options: UnregisterLocationOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const requestBody: UnregisterRequest = { name: options.name, }; @@ -196,7 +180,7 @@ export const unregisterLocation = async ( const response = await apiCall( "put", "/apiKey/v1/private-location/unregister", - config.apiKey, + options.apiKey, requestBody, ); @@ -214,16 +198,10 @@ export const unregisterLocation = async ( export const listPrivateLocations = async ( options: ListPrivateLocationsOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const response = await apiCall( "get", "/apiKey/v1/private-location", - config.apiKey, + options.apiKey, ); if (options.json) { @@ -242,16 +220,10 @@ export const listPrivateLocations = async ( export const listEnvironments = async ( options: ListEnvironmentsOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const response = await apiCall( "get", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, - config.apiKey, + options.apiKey, ); if (options.json) { @@ -271,12 +243,6 @@ export const listEnvironments = async ( export const createEnvironment = async ( options: CreateEnvironmentOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const requestBody = { name: options.name, discoveryUrl: options.discoveryUrl, @@ -289,7 +255,7 @@ export const createEnvironment = async ( const response = await apiCall( "post", `/apiKey/v2/test-targets/${options.testTargetId}/environments`, - config.apiKey, + options.apiKey, requestBody, ); @@ -308,12 +274,6 @@ export const createEnvironment = async ( export const updateEnvironment = async ( options: UpdateEnvironmentOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - const requestBody = { name: options.name, discoveryUrl: options.discoveryUrl, @@ -326,7 +286,7 @@ export const updateEnvironment = async ( const response = await apiCall( "patch", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, - config.apiKey, + options.apiKey, requestBody, ); @@ -345,16 +305,10 @@ export const updateEnvironment = async ( export const deleteEnvironment = async ( options: DeleteEnvironmentOptions, ): Promise => { - const config = await loadConfig(); - if (!config.apiKey) { - console.error("API key is required"); - process.exit(1); - } - await apiCall( "delete", `/apiKey/v2/test-targets/${options.testTargetId}/environments/${options.environmentId}`, - config.apiKey, + options.apiKey, ); if (options.json) { From d4a0cc3bef263748c1b59fe8e514ed40b7088fd9 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:38:13 +0200 Subject: [PATCH 07/12] readme, rename file --- README.md | 101 +++++++++++++++++--------- src/cli.ts | 2 +- src/{consoleActions.ts => helpers.ts} | 0 3 files changed, 68 insertions(+), 35 deletions(-) rename src/{consoleActions.ts => helpers.ts} (100%) diff --git a/README.md b/README.md index 07665df..0a65e35 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Continuous Integration](https://github.com/octomind-dev/cli/actions/workflows/ts.yml/badge.svg) -A command-line interface for interacting with the Octomind API. +A command-line interface for interacting with the Octomind API. This CLI allows you to execute tests, retrieve test reports, and manage private locations as well as environments. See [API documentation](https://octomind.dev/docs/api-reference/) @@ -13,33 +13,51 @@ See [API documentation](https://octomind.dev/docs/api-reference/) ## Commands +### Init + +Initialize configuration by setting up your API key and optionally a test target ID. This allows you to avoid passing these parameters for subsequent commands. + +```bash +octomind init \ + [--api-key ] \ + [--test-target-id ] \ + [--force] +``` + +Options: + +- `-k, --api-key`: Your Octomind API key (will prompt if not provided) +- `-t, --test-target-id`: Test target ID (optional) +- `-f, --force`: Force overwrite existing configuration + +If you don't provide the API key via the command line option, the CLI will prompt you to enter it interactively. Read the [docs](https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key) to learn how to generate an api key. + ### Execute Tests Run test cases against a specified URL. ```bash octomind execute \ - --api-key \ - --test-target-id \ --url \ + [--api-key ] \ + [--test-target-id ] \ [--environment ] \ [--tags ] \ [-v, --variables-to-overwrite ] \ [--description ] \ - [--tags ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + - `-u, --url` (required): URL to test +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-e, --environment`: Environment name (default: "default") - `-d, --description`: Test description - `-g, --tags`: comma separated list of tags for tests to execute - `-v, --variables-to-overwrite`: JSON object for variables to override for this run e.g. `{ "key": ["v1", "v2"]}` - `-j, --json`: Output raw JSON response -- `-g, --tags `: comma separated list of tags ### Get Test Report @@ -47,19 +65,21 @@ Retrieve details about a specific test report. ```bash octomind report \ - --api-key \ - --test-target-id \ --report-id \ + [--api-key ] \ + [--test-target-id ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + - `-r, --report-id` (required): Test report ID +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-j, --json`: Output raw JSON response Example text output: + ``` Test Report Details: Status: PASSED @@ -73,6 +93,7 @@ Test Results: ``` Example JSON output: + ```json { "id": "826c15af-644b-4b28-89b4-f50ff34e46b7", @@ -104,20 +125,21 @@ Register a new private location worker. If you use the [private location worker] ```bash octomind register-location \ - --api-key \ --name \ --proxypass \ --proxyuser \ --address
\ + [--api-key ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key + - `-n, --name` (required): Location name - `-p, --proxypass` (required): Proxy password - `-u, --proxyuser` (required): Proxy user - `-a, --address` (required): Location address (format: IP:PORT) +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-j, --json`: Output raw JSON response ### Unregister Private Location @@ -126,14 +148,15 @@ Remove a registered private location worker. If you use the [private location wo ```bash octomind unregister-location \ - --api-key \ --name \ + [--api-key ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key + - `-n, --name` (required): Location name +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-j, --json`: Output raw JSON response ### List Private Locations @@ -142,15 +165,17 @@ List all registered private locations. ```bash octomind list-private-locations \ - --api-key \ + [--api-key ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key + +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-j, --json`: Output raw JSON response Example text output: + ``` Private Locations: - Name: location-1 @@ -167,14 +192,15 @@ List all available environments. ```bash octomind list-environments \ - --api-key \ - --test-target-id \ + [--api-key ] \ + [--test-target-id ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-j, --json`: Output raw JSON response ### Create Environment @@ -183,10 +209,10 @@ Create a new environment for a test target. ```bash octomind create-environment \ - --api-key \ - --test-target-id \ --name \ --discovery-url \ + [--api-key ] \ + [--test-target-id ] \ [--test-account-username ] \ [--test-account-password ] \ [--test-account-otp-initializer-key ] \ @@ -198,10 +224,11 @@ octomind create-environment \ ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + - `-n, --name` (required): Environment name - `-d, --discovery-url` (required): Discovery URL +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `--test-account-username`: Test account username - `--test-account-password`: Test account password - `--test-account-otp-initializer-key`: OTP initializer key for test account @@ -217,9 +244,9 @@ Update an existing environment. ```bash octomind update-environment \ - --api-key \ - --test-target-id \ --environment-id \ + [--api-key ] \ + [--test-target-id ] \ [--name ] \ [--discovery-url ] \ [--test-account-username ] \ @@ -233,9 +260,10 @@ octomind update-environment \ ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + - `-e, --environment-id` (required): Environment ID +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-n, --name`: New environment name - `-d, --discovery-url`: New discovery URL - `--test-account-username`: New test account username @@ -253,16 +281,17 @@ Delete an existing environment. ```bash octomind delete-environment \ - --api-key \ - --test-target-id \ --environment-id \ + [--api-key ] \ + [--test-target-id ] \ [--json] ``` Options: -- `-k, --api-key` (required): Your Octomind API key -- `-t, --test-target-id` (required): Test target ID + - `-e, --environment-id` (required): Environment ID +- `-k, --api-key`: Your Octomind API key (optional if configured via `init`) +- `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-j, --json`: Output raw JSON response ## Output Formats @@ -270,6 +299,7 @@ Options: By default, the CLI provides formatted text output for better readability. Add the `--json` flag to any command to get the raw JSON response instead. This is useful for scripting or when you need to process the output programmatically. Example of JSON output: + ```json { "id": "826c15af-644b-4b28-89b4-f50ff34e46b7", @@ -299,15 +329,18 @@ Example of JSON output: 1. Clone the repository 2. Install dependencies: + ```bash pnpm install ``` The CLI is written in TypeScript and uses the following dependencies: + - `commander`: For command-line argument parsing - `axios`: For making HTTP requests To build from source: + ```bash pnpm run build -``` \ No newline at end of file +``` diff --git a/src/cli.ts b/src/cli.ts index 5f1895e..abe8755 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,7 +12,7 @@ import { updateEnvironment, } from "./api"; import { Config, loadConfig, saveConfig } from "./config"; -import { promptUser } from "./consoleActions"; +import { promptUser } from "./helpers"; const createCommandWithCommonOptions = (command: string): Command => { return program diff --git a/src/consoleActions.ts b/src/helpers.ts similarity index 100% rename from src/consoleActions.ts rename to src/helpers.ts From 7e460460231898d138b2cb27ccd0e4758e09c0ea Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:37:05 +0200 Subject: [PATCH 08/12] resolve test target id --- src/cli.ts | 16 +++++++++++++--- src/helpers.ts | 17 +++++++++++++++++ src/tools.ts | 28 +++++++++++++++++++--------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a72c3a4..33740fe 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,7 +12,7 @@ import { updateEnvironment, } from "./tools"; import { Config, loadConfig, saveConfig } from "./config"; -import { promptUser } from "./helpers"; +import { promptUser, resolveTestTargetId } from "./helpers"; const createCommandWithCommonOptions = (command: string): Command => { return program @@ -111,7 +111,12 @@ export const buildCmd = (): Command => { "JSON object of variables to overwrite", toJSON, ) - .action(executeTests); + .action((_, options) => + executeTests({ + ...options, + testTargetId: resolveTestTargetId(options.testTargetId), + }), + ); createCommandWithCommonOptions("test-report") .description("Get test report details") @@ -139,7 +144,12 @@ export const buildCmd = (): Command => { createCommandWithCommonOptions("list-environments") .description("List all environments") .option("-t, --test-target-id ", "Test target ID") - .action(listEnvironments); + .action((_, options) => + listEnvironments({ + ...options, + testTargetId: resolveTestTargetId(options.testTargetId), + }), + ); createCommandWithCommonOptions("create-environment") .description("Create a new environment") diff --git a/src/helpers.ts b/src/helpers.ts index 2d51de3..efaf28c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,6 @@ import { createInterface } from "node:readline"; import { stdin as input, stdout as output } from "node:process"; +import { loadConfig } from "./config"; export function promptUser(question: string): Promise { const rl = createInterface({ input, output }); @@ -11,3 +12,19 @@ export function promptUser(question: string): Promise { }); }); } + +export const resolveTestTargetId = async ( + providedTestTargetId?: string, +): Promise => { + if (providedTestTargetId) { + return providedTestTargetId; + } + + const config = await loadConfig(); + if (!config.testTargetId) { + throw new Error( + "testTargetId is required. Please provide it as a parameter or configure it first by running 'octomind init'", + ); + } + return config.testTargetId; +}; diff --git a/src/tools.ts b/src/tools.ts index 7af6a08..c9f6f69 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,5 +1,7 @@ import type { paths, components } from "./api"; // generated by openapi-typescript import createClient, { Middleware } from "openapi-fetch"; +import { loadConfig } from "./config"; +import { resolveTestTargetId } from "./helpers"; export type listEnvironmentsOptions = paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"]; @@ -21,15 +23,17 @@ export type EnvironmentResponse = components["schemas"]["EnvironmentResponse"]; export type ErrorResponse = components["schemas"]["ZodResponse"] | undefined; -const BASE_URL = "https://app.octomind.dev/api"; +const BASE_URL = "https://preview.octomind.dev/api"; const client = createClient({ baseUrl: BASE_URL }); const authMiddleware: Middleware = { async onRequest({ request }) { - const apiKey = process.env.APIKEY; + const { apiKey } = await loadConfig(); if (!apiKey) { - throw new Error("APIKEY environment variable is not set"); + throw new Error( + "API key is required. Please configure it first by running 'octomind init'", + ); } request.headers.set("x-api-key", apiKey); return request; @@ -47,11 +51,12 @@ const handleError = (error: ErrorResponse) => { export const listEnvironments = async ( options: listEnvironmentsOptions & { json?: boolean }, ): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/environments", { params: { - path: { testTargetId: options.testTargetId }, + path: { testTargetId }, }, }, ); @@ -81,9 +86,10 @@ const outputResult = (result: unknown): void => { export const executeTests = async ( options: executeTestsBody & { json?: boolean; description?: string }, ): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.POST("/apiKey/v2/execute", { body: { - testTargetId: options.testTargetId, + testTargetId, url: options.url, context: { source: "manual", @@ -129,12 +135,13 @@ export const executeTests = async ( export const getTestReport = async ( options: getTestReportParams & { json?: boolean }, ): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}", { params: { path: { - testTargetId: options.testTargetId, + testTargetId, testReportId: options.testReportId, }, }, @@ -259,11 +266,12 @@ export const createEnvironment = async ( testAccountOtpInitializerKey?: string; }, ): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.POST( "/apiKey/v2/test-targets/{testTargetId}/environments", { params: { - path: { testTargetId: options.testTargetId }, + path: { testTargetId }, }, body: { name: options.name, @@ -312,12 +320,13 @@ export const updateEnvironment = async ( testAccountOtpInitializerKey?: string; }, ): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.PATCH( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", { params: { path: { - testTargetId: options.testTargetId, + testTargetId, environmentId: options.environmentId, }, }, @@ -359,12 +368,13 @@ export const deleteEnvironment = async (options: { environmentId: string; json?: boolean; }): Promise => { + const testTargetId = await resolveTestTargetId(options.testTargetId); const { error } = await client.DELETE( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", { params: { path: { - testTargetId: options.testTargetId, + testTargetId, environmentId: options.environmentId, }, }, From 0058b3a430eedc72b95ef67d3cd37cfed90a39f1 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:54:20 +0200 Subject: [PATCH 09/12] tests --- src/cli.ts | 28 +++++++++++++++++++++------- tests/cli.spec.ts | 39 +++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 33740fe..ccb9d7b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,7 +20,7 @@ const createCommandWithCommonOptions = (command: string): Command => { .option("-j, --json", "Output raw JSON response"); }; -const splitter = (value: string): string[] => value.split(/,| /); +const splitter = (value: string): string[] => value.split(/,| |\|/); const toJSON = (value: string): object => JSON.parse(value); export const buildCmd = (): Command => { @@ -111,12 +111,26 @@ export const buildCmd = (): Command => { "JSON object of variables to overwrite", toJSON, ) - .action((_, options) => - executeTests({ - ...options, - testTargetId: resolveTestTargetId(options.testTargetId), - }), - ); + .action(async (command, options) => { + const testTargetId = await resolveTestTargetId(options.testTargetId); + await executeTests({ + url: command.url, + testTargetId, + environmentName: command.environmentName, + description: command.description, + tags: command.tags, + variablesToOverwrite: command.variablesToOverwrite, + json: command.json, + context: { + source: "manual", + description: command.description || "CLI execution", + triggeredBy: { + type: "USER", + userId: "cli-user", + }, + }, + }); + }); createCommandWithCommonOptions("test-report") .description("Get test report details") diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts index 3b7038e..5be72fb 100644 --- a/tests/cli.spec.ts +++ b/tests/cli.spec.ts @@ -4,7 +4,15 @@ import { executeTests } from "../src/tools"; jest.mock("../src/tools"); describe("CLI Commands parsing options", () => { - const stdArgs = ["node", "cli.js", "execute", "--url", "https://example.com"]; + const stdArgs = [ + "node", + "cli.js", + "execute", + "--url", + "https://example.com", + "--test-target-id", + "test-target-123", + ]; beforeAll(() => { buildCmd(); @@ -13,48 +21,48 @@ describe("CLI Commands parsing options", () => { }); }); - it("should parse executeTests tags option with comma", () => { - program.parse([...stdArgs, "--tags", "tag1,tags2"]); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should parse executeTests tags option with comma", async () => { + await program.parseAsync([...stdArgs, "--tags", "tag1,tags2"]); expect(executeTests).toHaveBeenCalledWith( expect.objectContaining({ tags: ["tag1", "tags2"], }), - expect.anything(), ); }); - it("should parse executeTests tags option with space", () => { - program.parse([...stdArgs, "--tags", "tag1 tags2"]); + it("should parse executeTests tags option with space", async () => { + await program.parseAsync([...stdArgs, "--tags", "tag1 tags2"]); expect(executeTests).toHaveBeenCalledWith( expect.objectContaining({ tags: ["tag1", "tags2"], }), - expect.anything(), ); }); - it("should parse executeTests tags option with |", () => { - program.parse([...stdArgs, "--tags", "tag1|tags2"]); + it("should parse executeTests tags option with |", async () => { + await program.parseAsync([...stdArgs, "--tags", "tag1|tags2"]); expect(executeTests).toHaveBeenCalledWith( expect.objectContaining({ tags: ["tag1", "tags2"], }), - expect.anything(), ); }); - it("should parse executeTests tags option always as array", () => { - program.parse([...stdArgs, "--tags", "tag1"]); + it("should parse executeTests tags option always as array", async () => { + await program.parseAsync([...stdArgs, "--tags", "tag1"]); expect(executeTests).toHaveBeenCalledWith( expect.objectContaining({ tags: ["tag1"], }), - expect.anything(), ); }); - it("should parse executeTests vars option as JSON", () => { - program.parse([ + it("should parse executeTests vars option as JSON", async () => { + await program.parseAsync([ ...stdArgs, "--variables-to-overwrite", '{ "foo": ["bar"] }', @@ -63,7 +71,6 @@ describe("CLI Commands parsing options", () => { expect.objectContaining({ variablesToOverwrite: { foo: ["bar"] }, }), - expect.anything(), ); }); }); From 98ad472bbedc15fcf5c83edec53a0216c3f39814 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:58:04 +0200 Subject: [PATCH 10/12] refactor --- src/cli.ts | 37 ++++++++++++++++++++----------------- tests/cli.spec.ts | 1 + 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index ccb9d7b..6b5d0e4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -114,21 +114,8 @@ export const buildCmd = (): Command => { .action(async (command, options) => { const testTargetId = await resolveTestTargetId(options.testTargetId); await executeTests({ - url: command.url, + ...command, testTargetId, - environmentName: command.environmentName, - description: command.description, - tags: command.tags, - variablesToOverwrite: command.variablesToOverwrite, - json: command.json, - context: { - source: "manual", - description: command.description || "CLI execution", - triggeredBy: { - type: "USER", - userId: "cli-user", - }, - }, }); }); @@ -136,7 +123,13 @@ export const buildCmd = (): Command => { .description("Get test report details") .requiredOption("-r, --test-report-id ", "Test report ID") .option("-t, --test-target-id ", "Test target ID") - .action(getTestReport); + .action(async (command, options) => { + const testTargetId = await resolveTestTargetId(options.testTargetId); + await getTestReport({ + ...command, + testTargetId, + }); + }); createCommandWithCommonOptions("register-location") .description("Register a private location") @@ -179,7 +172,12 @@ export const buildCmd = (): Command => { .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") .option("--private-location-name ", "Private location name") - .action(createEnvironment); + .action((_, options) => + createEnvironment({ + ...options, + testTargetId: resolveTestTargetId(options.testTargetId), + }), + ); createCommandWithCommonOptions("update-environment") .description("Update an existing environment") @@ -196,7 +194,12 @@ export const buildCmd = (): Command => { .option("--basic-auth-username ", "Basic auth username") .option("--basic-auth-password ", "Basic auth password") .option("--private-location-name ", "Private location name") - .action(updateEnvironment); + .action((_, options) => + updateEnvironment({ + ...options, + testTargetId: resolveTestTargetId(options.testTargetId), + }), + ); createCommandWithCommonOptions("delete-environment") .description("Delete an environment") diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts index 5be72fb..4549028 100644 --- a/tests/cli.spec.ts +++ b/tests/cli.spec.ts @@ -1,6 +1,7 @@ import { program } from "commander"; import { buildCmd } from "../src/cli"; import { executeTests } from "../src/tools"; + jest.mock("../src/tools"); describe("CLI Commands parsing options", () => { From abe23a46c974c8ce3d76344184dc861f844bcec7 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:13:27 +0200 Subject: [PATCH 11/12] fix tests, refactor --- src/cli.ts | 8 ++++---- src/tools.ts | 19 ++++++------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 6b5d0e4..6a01c56 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -111,10 +111,10 @@ export const buildCmd = (): Command => { "JSON object of variables to overwrite", toJSON, ) - .action(async (command, options) => { + .action(async (options) => { const testTargetId = await resolveTestTargetId(options.testTargetId); await executeTests({ - ...command, + ...options, testTargetId, }); }); @@ -123,10 +123,10 @@ export const buildCmd = (): Command => { .description("Get test report details") .requiredOption("-r, --test-report-id ", "Test report ID") .option("-t, --test-target-id ", "Test target ID") - .action(async (command, options) => { + .action(async (options) => { const testTargetId = await resolveTestTargetId(options.testTargetId); await getTestReport({ - ...command, + ...options, testTargetId, }); }); diff --git a/src/tools.ts b/src/tools.ts index c9f6f69..6f2c876 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,7 +1,6 @@ import type { paths, components } from "./api"; // generated by openapi-typescript import createClient, { Middleware } from "openapi-fetch"; import { loadConfig } from "./config"; -import { resolveTestTargetId } from "./helpers"; export type listEnvironmentsOptions = paths["/apiKey/v2/test-targets/{testTargetId}/environments"]["get"]["parameters"]["path"]; @@ -51,12 +50,11 @@ const handleError = (error: ErrorResponse) => { export const listEnvironments = async ( options: listEnvironmentsOptions & { json?: boolean }, ): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/environments", { params: { - path: { testTargetId }, + path: { testTargetId: options.testTargetId }, }, }, ); @@ -86,10 +84,9 @@ const outputResult = (result: unknown): void => { export const executeTests = async ( options: executeTestsBody & { json?: boolean; description?: string }, ): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.POST("/apiKey/v2/execute", { body: { - testTargetId, + testTargetId: options.testTargetId, url: options.url, context: { source: "manual", @@ -135,13 +132,12 @@ export const executeTests = async ( export const getTestReport = async ( options: getTestReportParams & { json?: boolean }, ): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.GET( "/apiKey/v2/test-targets/{testTargetId}/test-reports/{testReportId}", { params: { path: { - testTargetId, + testTargetId: options.testTargetId, testReportId: options.testReportId, }, }, @@ -266,12 +262,11 @@ export const createEnvironment = async ( testAccountOtpInitializerKey?: string; }, ): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.POST( "/apiKey/v2/test-targets/{testTargetId}/environments", { params: { - path: { testTargetId }, + path: { testTargetId: options.testTargetId }, }, body: { name: options.name, @@ -320,13 +315,12 @@ export const updateEnvironment = async ( testAccountOtpInitializerKey?: string; }, ): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { data, error } = await client.PATCH( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", { params: { path: { - testTargetId, + testTargetId: options.testTargetId, environmentId: options.environmentId, }, }, @@ -368,13 +362,12 @@ export const deleteEnvironment = async (options: { environmentId: string; json?: boolean; }): Promise => { - const testTargetId = await resolveTestTargetId(options.testTargetId); const { error } = await client.DELETE( "/apiKey/v2/test-targets/{testTargetId}/environments/{environmentId}", { params: { path: { - testTargetId, + testTargetId: options.testTargetId, environmentId: options.environmentId, }, }, From 6af86acd4698772ef8333ccd88897b29733f0cd3 Mon Sep 17 00:00:00 2001 From: Fabio <161820267+fabiomaienschein@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:51:51 +0200 Subject: [PATCH 12/12] update readme --- README.md | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0a65e35..0aced45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Continuous Integration](https://github.com/octomind-dev/cli/actions/workflows/ts.yml/badge.svg) -A command-line interface for interacting with the Octomind API. +A command-line interface for interacting with the Octomind API. This CLI allows you to execute tests, retrieve test reports, and manage private locations as well as environments. See [API documentation](https://octomind.dev/docs/api-reference/) @@ -25,12 +25,11 @@ octomind init \ ``` Options: - - `-k, --api-key`: Your Octomind API key (will prompt if not provided) - `-t, --test-target-id`: Test target ID (optional) - `-f, --force`: Force overwrite existing configuration -If you don't provide the API key via the command line option, the CLI will prompt you to enter it interactively. Read the [docs](https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key) to learn how to generate an api key. +If you don't provide the API key via the command line option, the CLI will prompt you to enter it interactively. You can get your API key from https://octomind.dev/docs/run-tests/execution-curl#create-an-api-key. ### Execute Tests @@ -49,7 +48,6 @@ octomind execute \ ``` Options: - - `-u, --url` (required): URL to test - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-t, --test-target-id`: Test target ID (optional if configured via `init`) @@ -72,14 +70,12 @@ octomind report \ ``` Options: - - `-r, --report-id` (required): Test report ID - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-j, --json`: Output raw JSON response Example text output: - ``` Test Report Details: Status: PASSED @@ -93,7 +89,6 @@ Test Results: ``` Example JSON output: - ```json { "id": "826c15af-644b-4b28-89b4-f50ff34e46b7", @@ -134,7 +129,6 @@ octomind register-location \ ``` Options: - - `-n, --name` (required): Location name - `-p, --proxypass` (required): Proxy password - `-u, --proxyuser` (required): Proxy user @@ -154,7 +148,6 @@ octomind unregister-location \ ``` Options: - - `-n, --name` (required): Location name - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-j, --json`: Output raw JSON response @@ -170,12 +163,10 @@ octomind list-private-locations \ ``` Options: - - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-j, --json`: Output raw JSON response Example text output: - ``` Private Locations: - Name: location-1 @@ -198,7 +189,6 @@ octomind list-environments \ ``` Options: - - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-t, --test-target-id`: Test target ID (optional if configured via `init`) - `-j, --json`: Output raw JSON response @@ -224,7 +214,6 @@ octomind create-environment \ ``` Options: - - `-n, --name` (required): Environment name - `-d, --discovery-url` (required): Discovery URL - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) @@ -260,7 +249,6 @@ octomind update-environment \ ``` Options: - - `-e, --environment-id` (required): Environment ID - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-t, --test-target-id`: Test target ID (optional if configured via `init`) @@ -288,7 +276,6 @@ octomind delete-environment \ ``` Options: - - `-e, --environment-id` (required): Environment ID - `-k, --api-key`: Your Octomind API key (optional if configured via `init`) - `-t, --test-target-id`: Test target ID (optional if configured via `init`) @@ -299,7 +286,6 @@ Options: By default, the CLI provides formatted text output for better readability. Add the `--json` flag to any command to get the raw JSON response instead. This is useful for scripting or when you need to process the output programmatically. Example of JSON output: - ```json { "id": "826c15af-644b-4b28-89b4-f50ff34e46b7", @@ -329,18 +315,14 @@ Example of JSON output: 1. Clone the repository 2. Install dependencies: - ```bash pnpm install ``` The CLI is written in TypeScript and uses the following dependencies: - - `commander`: For command-line argument parsing -- `axios`: For making HTTP requests To build from source: - ```bash pnpm run build -``` +``` \ No newline at end of file