diff --git a/.gitignore b/.gitignore index 78d7e13..6b8ca7e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules/ octomind.config.json octomind-cli-debug/ openapi.yaml +vhs/ diff --git a/README-template.md b/README-template.md index 97378be..2273bb4 100644 --- a/README-template.md +++ b/README-template.md @@ -6,6 +6,8 @@ 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/) + + ## Usage / Installation 1. To install the package globally do **NOT** just a `npm i -g @octomind/octomind` but instead diff --git a/README.md b/README.md index 4441198..9619df2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ 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/) + + ## Usage / Installation 1. To install the package globally do **NOT** just a `npm i -g @octomind/octomind` but instead @@ -40,20 +42,16 @@ This way even entityIds like environmentIds or testCaseIds will be autocompleted # octomind -Octomind cli tool. Version: 1.3.0. Additional documentation see https://octomind.dev/docs/api-reference/ +Octomind cli tool. Version: 1.3.1. Additional documentation see https://octomind.dev/docs/api-reference/ **Usage:** `octomind [options] [command]` ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-V, --version` | output the version number | No | | -# octomind CLI Documentation - -Octomind cli tool. Version: 1.3.0. Additional documentation see https://octomind.dev/docs/api-reference/ - ## Setup ## init @@ -65,7 +63,7 @@ Initialize configuration by setting up API key. This will create a config file i ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-t, --test-target-id ` | Test target ID | Yes | | | `-k, --api-key ` | the api key for authentication | Yes | | | `-f, --force` | Force overwrite existing configuration | No | | @@ -107,7 +105,7 @@ List all environments ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -120,7 +118,7 @@ Create a new environment ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-n, --name ` | Environment name | Yes | | | `-d, --discovery-url ` | Discovery URL | Yes | | @@ -141,7 +139,7 @@ Get an environment ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-e, --environment-id ` | Environment ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -155,7 +153,7 @@ Update an existing environment ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-e, --environment-id ` | Environment ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -177,7 +175,7 @@ Delete an environment ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-e, --environment-id ` | Environment ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -193,7 +191,7 @@ run test cases against local build ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-u, --url ` | url the tests should run against | Yes | | | `-c, --test-case-id [uuid]` | id of the test case you want to run, if not provided will run all test cases in the test target | No | | @@ -212,7 +210,7 @@ Execute test cases to create a test report ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-u, --url ` | URL to test | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -230,7 +228,7 @@ Create a new test case discovery ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-n, --name ` | Discovery name | Yes | | | `-p, --prompt ` | Discovery prompt | Yes | | @@ -252,7 +250,7 @@ Get notifications for a test target ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -267,7 +265,7 @@ Register a private location ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-n, --name ` | Location name | Yes | | | `-p, --password ` | Proxy password | Yes | | @@ -283,7 +281,7 @@ Unregister a private location ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-n, --name ` | Location name | Yes | | @@ -296,7 +294,7 @@ List all private locations ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | ## start-private-location @@ -308,7 +306,7 @@ Start a private location worker, see https://octomind.dev/docs/proxy/private-loc ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-n, --name [name]` | Location name | No | | | `-u, --username [username]` | Proxy user | No | | | `-p, --password [password]` | Proxy password | No | | @@ -331,7 +329,7 @@ Delete a test case ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-c, --test-case-id ` | Test case ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -345,7 +343,7 @@ Get details of a specific test case ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-c, --test-case-id ` | Test case ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -359,7 +357,7 @@ List all test cases ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -374,7 +372,7 @@ Get test report details ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | | `-r, --test-report-id ` | Test report ID | Yes | | | `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | | @@ -390,7 +388,7 @@ List all test targets ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | `-j, --json` | Output raw JSON response | No | | diff --git a/assets/octomind-cli-opt.gif b/assets/octomind-cli-opt.gif new file mode 100644 index 0000000..57f798b Binary files /dev/null and b/assets/octomind-cli-opt.gif differ diff --git a/package.json b/package.json index 8c5af45..09dbcaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "1.3.0", + "version": "1.3.1", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@10.13.1", diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index ff66fb6..c859a91 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -28,7 +28,7 @@ export function generateCommandDocs(command: Command, level = 1): string { if (options && options.length > 0) { docs += '### Options\n\n'; docs += '| Option | Description | Required | Default |\n'; - docs += '|--------|-------------|----------|--------|\n'; + docs += '|:-------|:----------|:---------|:--------|\n'; options.forEach(option => { const flags = option.flags; @@ -46,9 +46,6 @@ export function generateCommandDocs(command: Command, level = 1): string { const subcommands = command.commands; if (subcommands && subcommands.length > 0) { if (level === 1) { - docs += `# ${command.name()} CLI Documentation\n\n`; - docs += `${command.description()}\n\n`; - const commandsByGroup = new Map(); const ungroupedCommands: Command[] = []; diff --git a/src/cli.ts b/src/cli.ts index b08e8c4..40900dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -37,7 +37,8 @@ import { unregisterLocation, updateEnvironment, } from "./tools"; -import { getTestTargets, listTestTargets } from "./tools/test-targets"; +import { init, switchTestTarget } from "./tools/init"; +import { listTestTargets } from "./tools/test-targets"; import { version } from "./version"; export const BINARY_NAME = "octomind"; @@ -60,38 +61,6 @@ const addTestTargetWrapper = void fn({ ...options, testTargetId: resolvedTestTargetId }); }; -const selectTestTarget = async (): Promise => { - const testTargets = await getTestTargets(); - await listTestTargets({}); - - if (testTargets.length === 1) { - console.log( - `Only one test target found, using it: ${testTargets[0].app} (${testTargets[0].id})`, - ); - return testTargets[0].id; - } - - const testTargetIndex = await promptUser( - "Enter number of the test target you want to use (optional, press Enter to skip): ", - ); - const testTargetIndexAsInt = Number.parseInt(testTargetIndex); - - if ( - Number.isNaN(testTargetIndexAsInt) || - testTargetIndexAsInt < 1 || - testTargetIndexAsInt > testTargets.length - ) { - console.log("āŒ could not find a test target with the index you provided"); - process.exit(1); - } - const testTargetId = testTargets[testTargetIndexAsInt - 1].id; - if (!testTargetId) { - console.log("āŒ could not find a test target with the index you provided"); - process.exit(1); - } - - return testTargetId; -}; const testTargetIdOption = new Option( "-t, --test-target-id [id]", "Test target ID, if not provided will use the test target id from the config", @@ -127,71 +96,7 @@ export const buildCmd = (): CompletableCommand => { .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: { - testTargetId?: string; - apiKey: string; - force?: boolean; - }) => { - try { - console.log("šŸš€ Initializing configuration...\n"); - - const existingConfig = await loadConfig(options.force); - - 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; - } - } - - let apiKey: string = ""; - 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); - } - } - // saving here to be able to use the api key for the test targets - const newApiKeyConfig = { - ...existingConfig, - - apiKey: options.apiKey, - }; - - await saveConfig(newApiKeyConfig); - - const testTargetId = await selectTestTarget(); - - const newConfig: Config = { - ...existingConfig, - apiKey: options.apiKey, - testTargetId: options.testTargetId ?? testTargetId, - }; - - await saveConfig(newConfig); - - console.log("\n✨ Initialization complete!"); - } catch (error) { - console.error( - "āŒ Error during initialization:", - (error as Error).message, - ); - process.exit(1); - } - }, - ); + .action(init); program .completableCommand("switch-test-target") @@ -199,16 +104,7 @@ export const buildCmd = (): CompletableCommand => { "Switch to a different test target. This will list all available test targets and update the config file in ~/.config/octomind.json", ) .helpGroup("setup") - .action(async () => { - const testTargetId = await selectTestTarget(); - const existingConfig = await loadConfig(); - const newConfig: Config = { - ...existingConfig, - testTargetId, - }; - await saveConfig(newConfig); - console.log(`✨ Switched to test target: ${testTargetId}`); - }); + .action(switchTestTarget); createCommandWithCommonOptions(program, "debug") .completer(environmentIdCompleter) diff --git a/src/config.ts b/src/config.ts index c39ce49..ebc42f4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -59,6 +59,7 @@ export async function saveConfig(newConfig: Config): Promise { const configPath = await getConfigPath(true); await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2), "utf8"); console.log(`āœ… Configuration saved to ${configPath}`); + configLoaded = false; } catch (error) { console.error("āŒ Error saving configuration:", (error as Error).message); process.exit(1); diff --git a/src/tools/init.ts b/src/tools/init.ts new file mode 100644 index 0000000..a815fd2 --- /dev/null +++ b/src/tools/init.ts @@ -0,0 +1,107 @@ +import { Config, loadConfig, saveConfig } from "../config"; +import { promptUser } from "../helpers"; +import { getTestTargets, listTestTargets } from "./test-targets"; + +const selectTestTarget = async (): Promise => { + const testTargets = await getTestTargets(); + await listTestTargets({}); + + if (testTargets.length === 1) { + console.log( + `Only one test target found, using it: ${testTargets[0].app} (${testTargets[0].id})`, + ); + return testTargets[0].id; + } + + const testTargetIndex = await promptUser( + "Enter number of the test target you want to use (optional, press Enter to skip): ", + ); + const testTargetIndexAsInt = Number.parseInt(testTargetIndex); + + if ( + Number.isNaN(testTargetIndexAsInt) || + testTargetIndexAsInt < 1 || + testTargetIndexAsInt > testTargets.length + ) { + console.log("āŒ could not find a test target with the index you provided"); + process.exit(1); + } + const testTargetId = testTargets[testTargetIndexAsInt - 1].id; + if (!testTargetId) { + console.log("āŒ could not find a test target with the index you provided"); + process.exit(1); + } + + return testTargetId; +}; + +export const switchTestTarget = async () => { + const testTargetId = await selectTestTarget(); + const existingConfig = await loadConfig(); + const newConfig: Config = { + ...existingConfig, + testTargetId, + }; + await saveConfig(newConfig); + console.log(`✨ Switched to test target: ${testTargetId}`); +}; + +export const init = async (options: { + testTargetId?: string; + apiKey: string; + force?: boolean; +}) => { + try { + console.log("šŸš€ Initializing configuration...\n"); + + const existingConfig = await loadConfig(options.force); + + 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 (!options.apiKey) { + options.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 (!options.apiKey) { + console.log("āŒ API key is required."); + process.exit(1); + } + } + // saving here to be able to use the api key for the test targets + const newApiKeyConfig = { + ...existingConfig, + + apiKey: options.apiKey, + }; + + await saveConfig(newApiKeyConfig); + + const testTargetId = await selectTestTarget(); + + const newConfig: Config = { + ...existingConfig, + apiKey: options.apiKey, + testTargetId: options.testTargetId ?? testTargetId, + }; + + await saveConfig(newConfig); + + console.log("\n✨ Initialization complete!"); + } catch (error) { + console.error("āŒ Error during initialization:", (error as Error).message); + process.exit(1); + } +}; diff --git a/src/tools/test-cases.ts b/src/tools/test-cases.ts index 19ef1fa..9f4242e 100644 --- a/src/tools/test-cases.ts +++ b/src/tools/test-cases.ts @@ -125,6 +125,9 @@ export const getTestCases = async ( export const listTestCases = async ( options: GetTestCasesOptions & ListOptions, ): Promise => { + if (!options.status) { + options.status = "ENABLED"; + } const testCases = await getTestCases(options); if (options.json) { diff --git a/tests/scripts/generate-docs.spec.ts b/tests/scripts/generate-docs.spec.ts index 7025302..e9da33d 100644 --- a/tests/scripts/generate-docs.spec.ts +++ b/tests/scripts/generate-docs.spec.ts @@ -2,20 +2,23 @@ import { generateCommandDocs } from "../../scripts/generate-docs"; import { Command } from "commander"; describe("generateCommandDocs", () => { - const mockCommand = new Command(); + const mockCommand = new Command("test-command"); + mockCommand.version("1.0.0"); mockCommand.command("test").description("test").option("-t, --test", "test"); mockCommand.command("test2").description("test2").helpGroup("test"); mockCommand.command("test3").description("test3").helpGroup("setup"); it("should generate documentation", () => { const docs = generateCommandDocs(mockCommand); expect(docs).toMatchInlineSnapshot(` -"# +"# test-command -**Usage:** \` [options] [command]\` - -# CLI Documentation +**Usage:** \`test-command [options] [command]\` +### Options +| Option | Description | Required | Default | +|:-------|:----------|:---------|:--------| +| \`-V, --version\` | output the version number | No | | ## Setup @@ -44,7 +47,7 @@ test ### Options | Option | Description | Required | Default | -|--------|-------------|----------|--------| +|:-------|:----------|:---------|:--------| | \`-t, --test\` | test | No | | " diff --git a/tests/tools/init.spec.ts b/tests/tools/init.spec.ts new file mode 100644 index 0000000..3a3a892 --- /dev/null +++ b/tests/tools/init.spec.ts @@ -0,0 +1,45 @@ +import { init } from "../../src/tools/init"; +import { loadConfig, saveConfig } from "../../src/config"; +import { promptUser } from "../../src/helpers"; +import { getTestTargets } from "../../src/tools/test-targets"; +jest.mock("../../src/config"); +jest.mock("../../src/helpers"); +jest.mock("../../src/tools/test-targets"); +describe("init", () => { + const originalConsoleLog = console.log; + beforeAll(() => { + jest.clearAllMocks(); + console.log = jest.fn(); + }); + afterAll(() => { + console.log = originalConsoleLog; + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + const loadConfigMock = loadConfig as jest.Mock; + loadConfigMock.mockResolvedValue({ apiKey: "apiKey" }); + const promptUserMock = promptUser as jest.Mock; + promptUserMock.mockResolvedValue("1"); + const getTestTargetsMock = getTestTargets as jest.Mock; + getTestTargetsMock.mockResolvedValue([{ id: "testTargetId", app: "testTargetApp" }]); + + it("should initialize the configuration with one test target", async () => { + await init({ apiKey: "newApiKey", force: true }); + expect(loadConfigMock).toHaveBeenCalledWith(true); + expect(saveConfig).toHaveBeenCalledWith({ apiKey: "newApiKey", testTargetId: "testTargetId" }); + expect(console.log).toHaveBeenCalledWith("Only one test target found, using it: testTargetApp (testTargetId)"); + expect(console.log).toHaveBeenNthCalledWith(3, "\n✨ Initialization complete!"); + }); + + it("should initialize the configuration with multiple test targets", async () => { + getTestTargetsMock.mockResolvedValue([ + { id: "testTargetId1", app: "testTargetApp1" }, + { id: "testTargetId2", app: "testTargetApp2" }, + ]); + await init({ apiKey: "newApiKey", force: true }); + expect(loadConfigMock).toHaveBeenCalledWith(true); + expect(saveConfig).toHaveBeenCalledWith({ apiKey: "newApiKey", testTargetId: "testTargetId1" }); + expect(console.log).toHaveBeenNthCalledWith(2, "\n✨ Initialization complete!"); + }); +}); \ No newline at end of file