Skip to content
This repository was archived by the owner on Jul 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"author": "octomind",
"license": "MIT",
"dependencies": {
"@logtape/logtape": "2.0.0",
"@playwright/test": "1.57.0",
"@types/shell-quote": "1.7.5",
"commander": "14.0.2",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function generateCommandDocs(command: Command, level = 1): string {
*/
async function main() {
try {
const program = buildCmd();
const program = await buildCmd();

const markdown = generateCommandDocs(program);

Expand Down
5 changes: 4 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "./completion";
import { executeLocalTestCases, runDebugtopus } from "./debugtopus";
import { resolveTestTargetId } from "./helpers";
import { configureLogger } from "./logger";
import { startPrivateLocationWorker, stopPLW } from "./plw";
import {
batchGeneration,
Expand Down Expand Up @@ -77,7 +78,9 @@ const createCommandWithCommonOptions = (
.option("-j, --json", "Output raw JSON response") as CompletableCommand;
};

export const buildCmd = (): CompletableCommand => {
export const buildCmd = async (): Promise<CompletableCommand> => {
await configureLogger();

const program = new CompletableCommand();

program
Expand Down
5 changes: 3 additions & 2 deletions src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { install, log, parseEnv, TabtabEnv, uninstall } from "tabtab";

import { BINARY_NAME } from "./cli";
import { loadConfig } from "./config";
import { logger } from "./logger";
import { getTestReports } from "./tools";
import { getEnvironments } from "./tools/environments";
import { getTestCases } from "./tools/test-cases";
Expand Down Expand Up @@ -193,11 +194,11 @@ export const installCompletion = async () => {
await install({
name: BINARY_NAME,
completer: BINARY_NAME,
}).catch((err) => console.error("INSTALL ERROR", err));
}).catch((err) => logger.error("INSTALL ERROR", { err }));
};

export const uninstallCompletion = async () => {
await uninstall({
name: BINARY_NAME,
}).catch((err) => console.error("UNINSTALL ERROR", err));
}).catch((err) => logger.error("UNINSTALL ERROR", { err }));
};
11 changes: 5 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import fs from "fs/promises";
import { homedir } from "os";
import { join } from "path";

import { logger } from "./logger";

const CONFIG_DIR = ".config";

export async function getConfigPath(ensureDir?: boolean): Promise<string> {
Expand Down Expand Up @@ -45,10 +47,7 @@ export async function loadConfig(force?: boolean): Promise<Config> {
} catch (error) {
// only exit on overwrite attempt
if (force) {
console.error(
"❌ Error parsing configuration:",
(error as Error).message,
);
logger.error("❌ Error parsing configuration", { error });
process.exit(1);
}
return {};
Expand All @@ -59,10 +58,10 @@ export async function saveConfig(newConfig: Config): Promise<void> {
try {
const configPath = await getConfigPath(true);
await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2), "utf8");
console.log(`✅ Configuration saved to ${configPath}`);
logger.info(`✅ Configuration saved to ${configPath}`);
configLoaded = false;
} catch (error) {
console.error("❌ Error saving configuration:", (error as Error).message);
logger.error("❌ Error saving configuration", { error });
process.exit(1);
}
}
9 changes: 5 additions & 4 deletions src/debugtopus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
findOctomindFolder,
getAbsoluteFilePathInOctomindRoot,
} from "../helpers";
import { logger } from "../logger";
import {
getEnvironments,
getPlaywrightCode,
Expand Down Expand Up @@ -91,7 +92,7 @@ const prepareDirectories = async (
const appDir = dirname(nodeModule.filename);
packageRootDir = getPackageRootLevel(appDir);
} else {
console.warn("package was not installed as valid nodeJS module");
logger.warn("package was not installed as valid nodeJS module");
packageRootDir = process.cwd();
}
}
Expand Down Expand Up @@ -169,17 +170,17 @@ const runTests = async ({
command += " --ui";
}

console.log(`executing command : '${command}'`);
logger.info(`executing command : '${command}'`);

const { stderr } = await promisify(exec)(command, {
cwd: packageRootDir,
});

if (stderr) {
console.error(stderr);
logger.error(stderr);
process.exit(1);
} else {
console.log(`success, you can find your artifacts at ${outputDir}`);
logger.info(`success, you can find your artifacts at ${outputDir}`);
}
};

Expand Down
6 changes: 4 additions & 2 deletions src/debugtopus/installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { promisify } from "util";

import { chromium } from "@playwright/test";

import { logger } from "../logger";

export const ensureChromiumIsInstalled = async (
packageRootDir: string,
): Promise<void> => {
Expand All @@ -20,7 +22,7 @@ export const ensureChromiumIsInstalled = async (
reject(error);
return;
}
console.log(
logger.info(
"Couldn't find any chromium binary, executing 'npx playwright install chromium'",
);

Expand All @@ -32,7 +34,7 @@ export const ensureChromiumIsInstalled = async (
);

playwrightInstallExecution.child?.stdout?.on("data", (data) =>
console.log(data),
logger.info(data),
);

await playwrightInstallExecution;
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env node

import { buildCmd } from "./cli";
import { logger } from "./logger";

buildCmd().parse();
void buildCmd()
.then((res) => res.parse())
.catch(logger.error);
56 changes: 56 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
configure,
FormattedValues,
getConsoleSink,
getLogger,
getTextFormatter,
LogLevel,
} from "@logtape/logtape";

export const logger = getLogger("octomind");

const ansiColors = {
red: "\x1B[31m",
green: "\x1B[32m",
yellow: "\x1B[33m",
blue: "\x1B[34m",
magenta: "\x1B[35m",
cyan: "\x1B[36m",
// use native terminal colors
none: "",
};

const levelColors: Record<
LogLevel | string,
(typeof ansiColors)[keyof typeof ansiColors]
> = {
trace: ansiColors.none,
debug: ansiColors.none,
info: ansiColors.none,
warning: ansiColors.yellow,
error: ansiColors.red,
fatal: ansiColors.red,
};

export const configureLogger = async (): Promise<void> =>
configure({
sinks: {
console: getConsoleSink({
formatter: getTextFormatter({
timestamp: "disabled",
level: "full",
format: (values: FormattedValues): string => {
return levelColors[values.level] + values.message;
},
}),
}),
},
loggers: [
{
category: ["logtape", "meta"],
sinks: ["console"],
lowestLevel: "warning",
},
{ category: "octomind", lowestLevel: "debug", sinks: ["console"] },
],
});
17 changes: 9 additions & 8 deletions src/plw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { randomUUID } from "crypto";
import { createInterface } from "readline";

import { loadConfig } from "../config";
import { logger } from "../logger";

interface StreamResult {
lines: string[];
Expand Down Expand Up @@ -39,7 +40,7 @@ const spawnAndStreamLines = async (

rl.on("line", (line: string) => {
lines.push(line);
console.log(`-- ${line}`);
logger.info(`-- ${line}`);
lineCount++;

if (lineCount >= maxLines) {
Expand All @@ -58,7 +59,7 @@ const spawnAndStreamLines = async (

child.on("exit", (code, signal) => {
if (lineCount < maxLines) {
console.log(`Process exited with code ${code}, signal ${signal}`);
logger.info(`Process exited with code ${code}, signal ${signal}`);
}
resolve({ lines, detached: false, code, signal });
});
Expand Down Expand Up @@ -111,29 +112,29 @@ export const startPrivateLocationWorker = async (options: {
});

if (!(await checkDockerDaemon())) {
console.error(
logger.error(
"Docker daemon is not running. Please start Docker and try again.",
);
return;
}

console.log(
logger.info(
`executing command : '${command.replace(/APIKEY=[^\s&]*/g, "APIKEY=***")}'`,
);
const args = command.split(" ");
const result = await spawnAndStreamLines(args[0], args.slice(1), 10);
console.log(`Captured ${result.lines.length} lines`);
console.log(`Process detached: ${result.detached}`);
logger.info(`Captured ${result.lines.length} lines`);
logger.info(`Process detached: ${result.detached}`);
};

export const stopPLW = async (): Promise<void> => {
const command = "docker stop PLW";
const args = command.split(" ");
const result = await spawnAndStreamLines(args[0], args.slice(1), 10);
if (result.code === 0) {
console.log("Private Location Worker stopped successfully.");
logger.info("Private Location Worker stopped successfully.");
} else {
console.error(
logger.error(
`Failed to stop Private Location Worker. Exit code: ${result.code}`,
);
}
Expand Down
9 changes: 5 additions & 4 deletions src/tools/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import createClient, { Client, Middleware } from "openapi-fetch";

import type { components, paths } from "../api"; // generated by openapi-typescript
import { loadConfig } from "../config";
import { logger } from "../logger";
import { version } from "../version";

export const BASE_URL =
Expand Down Expand Up @@ -65,7 +66,7 @@ const createAuthMiddleware = ({
return response;
},
onError({ error }) {
console.error(error);
logger.error("error", { error });
process.exit(1);
},
};
Expand All @@ -77,9 +78,9 @@ export { client, paths };

export const handleError = (error: ErrorResponse) => {
if (error) {
console.error(error);
logger.error("error", { error });
if (typeof error === "string" && error.startsWith("403")) {
console.error(
logger.error(
"You are not authorized. Check your API key or do a 'octomind init' to set it up.",
);
}
Expand All @@ -91,5 +92,5 @@ export type ListOptions = {
json?: boolean;
};
export const logJson = (result: unknown): void => {
console.log(JSON.stringify(result, null, 2));
logger.info(JSON.stringify(result, null, 2));
};
15 changes: 8 additions & 7 deletions src/tools/discoveries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { components } from "../api"; // generated by openapi-typescript
import { logger } from "../logger";
import { getUrl } from "../url";
import { client, handleError, ListOptions, logJson } from "./client";

Expand Down Expand Up @@ -47,9 +48,9 @@ export const batchGeneration = async (
throw new Error("Batch generation ID is missing");
}

console.log("Batch generation created successfully!");
console.log(`Batch generation ID: ${response.batchGenerationId}`);
console.log(
logger.info("Batch generation created successfully!");
logger.info(`Batch generation ID: ${response.batchGenerationId}`);
logger.info(
`Batch generation URL: ${await getUrl({
batchGenerationId: response.batchGenerationId,
entityType: "batch-generation",
Expand Down Expand Up @@ -95,13 +96,13 @@ export const createDiscovery = async (
return;
}

console.log("Discovery created successfully!");
console.log(
logger.info("Discovery created successfully!");
logger.info(
`${await getUrl({
testCaseId: response.testCaseId ?? "",
entityType: "discovery",
})}`,
);
console.log(`Discovery ID: ${response.discoveryId}`);
console.log(`Test Case ID: ${response.testCaseId}`);
logger.info(`Discovery ID: ${response.discoveryId}`);
logger.info(`Test Case ID: ${response.testCaseId}`);
};
Loading