Skip to content
This repository was archived by the owner on Jul 14, 2026. It is now read-only.
Closed
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
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This way even entityIds like environmentIds or testCaseIds will be autocompleted

# octomind

Octomind cli tool. Version: 3.7.0. Additional documentation see https://octomind.dev/docs/api-reference/
Octomind cli tool. Version: 4.0.0. Additional documentation see https://octomind.dev/docs/api-reference/

**Usage:** `octomind [options] [command]`

Expand Down Expand Up @@ -238,12 +238,11 @@ Execute local YAML test cases
| `-j, --json` | Output raw JSON response | No | |
| `-u, --url <url>` | url the tests should run against | Yes | |
| `-e, --environment-id [uuid]` | id of the environment you want to run against, if not provided will run all test cases against the default environment | No | |
| `-t, --test-target-id [uuid]` | id of the test target of the test case, if not provided will use the test target id from the config | No | |
| `-t, --test-target-id [id]` | Test target ID, if not provided will use the test target id from the config | No | |
| `--headless` | if we should run headless without the UI of playwright and the browser | No | |
| `--bypass-proxy` | bypass proxy when accessing the test target | No | |
| `--browser [CHROMIUM, FIREFOX, SAFARI]` | Browser type | No | CHROMIUM |
| `--breakpoint [DESKTOP, MOBILE, TABLET]` | Breakpoint | No | DESKTOP |
| `-s, --source <path>` | Source directory (defaults to current directory) | Yes | ./.octomind |

## create-discovery

Expand Down Expand Up @@ -432,7 +431,6 @@ Pull test cases from the test target
|:-------|:----------|:---------|:--------|
| `-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 | |
| `-d, --destination <path>` | Destination folder | Yes | ./.octomind |

## push

Expand All @@ -446,7 +444,6 @@ Push local YAML test cases to the test target
|:-------|:----------|:---------|:--------|
| `-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 | |
| `-s, --source <path>` | Source directory (defaults to current directory) | Yes | .octomind |

## Test Reports

Expand Down Expand Up @@ -478,6 +475,24 @@ List all test targets
|:-------|:----------|:---------|:--------|
| `-j, --json` | Output raw JSON response | No | |

## Other Commands

## show-octomind-dir

**Usage:** `show-octomind-dir [options] [command]`

### Options

| Option | Description | Required | Default |
|:-------|:----------|:---------|:--------|
| `-j, --json` | Output raw JSON response | No | |

### show-octomind-dir

Show where the octomind directory containing your local test cases is located.

**Usage:** `show-octomind-dir [options]`



## Output Formats
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@octomind/octomind",
"version": "3.7.0",
"version": "4.0.0",
Comment thread
RunOrVeith marked this conversation as resolved.
"description": "a command line client for octomind apis",
"main": "./dist/index.js",
"packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402",
Expand Down
24 changes: 9 additions & 15 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
uninstallCompletion,
} from "./completion";
import { executeLocalTestCases, runDebugtopus } from "./debugtopus";
import { showOctomindDir } from "./dirManagement";
import { resolveTestTargetId } from "./helpers";
import { startPrivateLocationWorker, stopPLW } from "./plw";
import {
Expand Down Expand Up @@ -181,22 +182,14 @@ export const buildCmd = (): CompletableCommand => {
"-e, --environment-id [uuid]",
"id of the environment you want to run against, if not provided will run all test cases against the default environment",
)
.option(
"-t, --test-target-id [uuid]",
"id of the test target of the test case, if not provided will use the test target id from the config",
)
.addOption(testTargetIdOption)
.option(
"--headless",
"if we should run headless without the UI of playwright and the browser",
)
.option("--bypass-proxy", "bypass proxy when accessing the test target")
.option("--browser [CHROMIUM, FIREFOX, SAFARI]", "Browser type", "CHROMIUM")
.option("--breakpoint [DESKTOP, MOBILE, TABLET]", "Breakpoint", "DESKTOP")
.option(
"-s, --source <path>",
"Source directory (defaults to current directory)",
"./.octomind",
)
.action(addTestTargetWrapper(executeLocalTestCases));

createCommandWithCommonOptions(program, "test-report")
Expand Down Expand Up @@ -395,7 +388,6 @@ export const buildCmd = (): CompletableCommand => {
.description("Pull test cases from the test target")
.helpGroup("test-cases")
.addOption(testTargetIdOption)
.option("-d, --destination <path>", "Destination folder", "./.octomind")
.action(addTestTargetWrapper(pullTestTarget));

// noinspection RequiredAttributes
Expand All @@ -404,13 +396,15 @@ export const buildCmd = (): CompletableCommand => {
.description("Push local YAML test cases to the test target")
.helpGroup("test-cases")
.addOption(testTargetIdOption)
.option(
"-s, --source <path>",
"Source directory (defaults to current directory)",
".octomind",
)
.action(addTestTargetWrapper(pushTestTarget));

createCommandWithCommonOptions(program, "show-octomind-dir")
Comment thread
RunOrVeith marked this conversation as resolved.
.description(
"Show where the octomind directory containing your local test cases is located.",
)
.helpGroup("setup")
.action(showOctomindDir);

createCommandWithCommonOptions(program, "list-test-targets")
.description("List all test targets")
.helpGroup("test-targets")
Expand Down
14 changes: 12 additions & 2 deletions src/debugtopus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { promisify } from "util";

import { Open } from "unzipper";

import { getPathToOctomindDirWithActiveTestTarget } from "../dirManagement";
import {
getEnvironments,
getPlaywrightCode,
Expand Down Expand Up @@ -262,9 +263,18 @@ export const runDebugtopus = async (options: DebugtopusOptions) => {
};

export const executeLocalTestCases = async (
options: DebugtopusOptions & { source: string },
options: DebugtopusOptions & {
startDir?: string;
},
): Promise<void> => {
const testCases = readTestCasesFromDir(options.source);
const source = await getPathToOctomindDirWithActiveTestTarget({
startDir: options.startDir,
providedTestTargetId: options.testTargetId,
});
if (!source) {
throw new Error("No octomind directory found");
}
const testCases = readTestCasesFromDir(source);
const body = {
testCases,
testTargetId: options.testTargetId,
Expand Down
123 changes: 123 additions & 0 deletions src/dirManagement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { existsSync, mkdirSync, readdirSync, rmdirSync } from "node:fs";
import path from "node:path";

import { promptUser, resolveTestTargetId } from "./helpers";
import { getTestTargets } from "./tools";
import { toFileSystemCompatibleCamelCase } from "./tools/sync/yml";

export const OCTOMIND_DIR = ".octomind";

export const getPathToOctomindDirWithActiveTestTarget = async (options: {
startDir?: string;
providedTestTargetId?: string;
allowCreation?: boolean;
}): Promise<string | null> => {
const testTargetId = await resolveTestTargetId(options.providedTestTargetId);
const octomindDir = await getPathToOctomindDir({
allowCreation: options.allowCreation,
startDir: options.startDir,
});
if (!octomindDir) {
return null;
}
const testTargets = await getTestTargets();
const testTarget = testTargets.find((tt) => tt.id === testTargetId);
if (!testTarget) {
throw new Error(`Configured test target with id ${testTargetId} not found`);
}
const testTargetDirName = toFileSystemCompatibleCamelCase(testTarget.app);
const testTargetFolderPath = path.resolve(
path.join(octomindDir, testTargetDirName),
);
if (!existsSync(testTargetFolderPath)) {
mkdirSync(testTargetFolderPath, { recursive: true });
}
return testTargetFolderPath;
};

export const getPathToOctomindDir = async ({
allowCreation,
startDir = process.cwd(),
}: {
allowCreation?: boolean;
startDir?: string;
} = {}): Promise<string | null> => {
let currentDir = startDir;
// Make loop stop when reaching the root directory, in an OS-independent way
while (currentDir !== path.dirname(currentDir)) {
if (checkForOctomindDir(currentDir)) {
return path.resolve(path.join(currentDir, OCTOMIND_DIR));
}
currentDir = path.dirname(currentDir);
}
if (allowCreation) {
return createOctomindDirInteractive({ dir: startDir });
}
return null;
};

const checkForOctomindDir = (dir: string): boolean => {
const octomindDir = path.join(dir, OCTOMIND_DIR);
try {
return existsSync(octomindDir);
} catch (error) {
console.debug(`Error checking for octomind dir in ${dir}:`, error);
// likely a permission issue, so we return false
return false;
}
};

export const createOctomindDirInteractive = async (
options: { dir?: string; recreateOctomindDir?: boolean } = {},
): Promise<string> => {
const dir = path.resolve(
path.join(options.dir ?? process.cwd(), OCTOMIND_DIR),
);
const existingOctomindDir = await getPathToOctomindDir({
allowCreation: false,
startDir: options.dir,
});

if (
existingOctomindDir &&
existingOctomindDir !== dir &&
!options.recreateOctomindDir
) {
console.log(`Octomind directory already exists: ${existingOctomindDir}.`);
console.log(`Using existing octomind directory.`);
return existingOctomindDir;
}

if (existingOctomindDir !== null) {
console.log(`Octomind directory already exists: ${dir}.`);
const removeExisting =
options.recreateOctomindDir ||
(
await promptUser(`Remove and recreate empty directory? (y/N): `)
).toLowerCase() === "y";
if (removeExisting) {
console.log(`Removing and recreating empty directory...`);
rmdirSync(existingOctomindDir);
mkdirSync(dir, { recursive: true });
} else {
console.log(`Using existing octomind directory.`);
}
} else {
mkdirSync(dir, { recursive: true });
console.log(`✨ Created octomind directory: ${dir}`);
}
return dir;
};

export const showOctomindDir = async (): Promise<void> => {
const octomindDir = await getPathToOctomindDirWithActiveTestTarget({
allowCreation: false,
});
if (!octomindDir) {
console.log(
"You are not in an octomind directory or one of its children. Please run `octomind pull` to create one.",
);
return;
}
console.log(`Octomind directory: ${octomindDir}`);
};
4 changes: 3 additions & 1 deletion src/tools/sync/yml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const removeInvalidCharacters = (str: string): string => {
return removeDiacritics(str).replace(/[\\/:*?"'<>|.]/g, "");
};

const toFileSystemCompatibleCamelCase = (description: string): string => {
export const toFileSystemCompatibleCamelCase = (
description: string,
): string => {
const tokens = removeInvalidCharacters(description)
.split(/\s/)
.filter(Boolean);
Expand Down
29 changes: 22 additions & 7 deletions src/tools/test-targets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import path from "path";

import {
getPathToOctomindDir,
getPathToOctomindDirWithActiveTestTarget,
} from "../dirManagement";
import { getUrl } from "../url";
import { client, handleError, ListOptions, logJson } from "./client";
import { push } from "./sync/push";
Expand Down Expand Up @@ -44,7 +48,7 @@ export const listTestTargets = async (options: ListOptions): Promise<void> => {
};

export const pullTestTarget = async (
options: { testTargetId: string; destination?: string } & ListOptions,
options: { testTargetId: string } & ListOptions,
): Promise<void> => {
const { data, error } = await client.GET(
"/apiKey/beta/test-targets/{testTargetId}/pull",
Expand All @@ -68,21 +72,32 @@ export const pullTestTarget = async (
return;
}

writeYaml(data, options.destination);
const destination = await getPathToOctomindDirWithActiveTestTarget({
allowCreation: true,
providedTestTargetId: options.testTargetId,
});
if (!destination) {
throw new Error("No octomind directory found");
}
writeYaml(data, destination);

console.log("Test Target pulled successfully");
};

export const pushTestTarget = async (
options: { testTargetId: string; source?: string } & ListOptions,
options: { testTargetId: string } & ListOptions,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fyi, currently https://github.com/OctoMind-dev/automagically-action-execute/blob/v2.8.4/.github/workflows/main.yml#L126 this workflow relies on it being possible to configure a different source (so that the OTHER steps in that workflow do NOT use ymls)

We can solve that differently (by writing the ymls only in the step they should be used), but just so you are aware

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't get this comment, please come up with a solution in january ;) so that the OTHER steps in that workflow do NOT use ymls

): Promise<void> => {
const sourceDir = options.source
? path.resolve(options.source)
: process.cwd();
const source = await getPathToOctomindDirWithActiveTestTarget({
allowCreation: false,
providedTestTargetId: options.testTargetId,
});
if (!source) {
throw new Error("No octomind directory found");
}

const data = await push({
...options,
sourceDir,
sourceDir: source,
testTargetId: options.testTargetId,
onError: handleError,
client,
Expand Down
Loading