qazen-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # QAZen CLI
2
+
3
+ Capture authenticated browser sessions for enterprise app testing — including SSO, ADFS, Okta, Cognito, and MFA flows.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install
9
+ npm install -g qazen-cli
10
+
11
+ # Connect (get the token from QAZen → Settings → Authentication)
12
+ qazen login --token <your-cli-token> --api-url https://qa-zen-ai.replit.app
13
+
14
+ # Record a session (opens a real browser)
15
+ qazen record --project "My App"
16
+
17
+ # Check status
18
+ qazen status
19
+ ```
20
+
21
+ ## Why this works for HttpOnly cookies
22
+
23
+ Playwright's Chrome DevTools Protocol (CDP) has direct access to all browser state, including HttpOnly cookies that JavaScript cannot read. This is the only reliable way to capture enterprise SSO sessions.
24
+
25
+ Your credentials never reach QAZen servers — only the resulting session storage state is uploaded.
26
+
27
+ ## Commands
28
+
29
+ - `qazen login --token <key>` — store the CLI token locally
30
+ - `qazen record [--project <name>] [--url <url>]` — open a browser, let you log in, capture the session, upload to QAZen
31
+ - `qazen status` — verify connection and list projects
32
+
33
+ ## Config location
34
+
35
+ The CLI stores its API URL and token under `conf`'s standard location (`~/.config/qazen-cli/config.json` on Linux, `~/Library/Preferences/qazen-cli-nodejs/` on macOS, `%APPDATA%\qazen-cli-nodejs\` on Windows).
@@ -0,0 +1,32 @@
1
+ import chalk from "chalk";
2
+ import { saveConfig } from "../lib/config.js";
3
+ import { fetchProjects } from "../lib/api.js";
4
+ export async function loginCommand(options) {
5
+ if (!options.token) {
6
+ console.log(chalk.red("\n Error: --token is required"));
7
+ console.log(chalk.gray(" Usage: qazen login --token <your-cli-token>\n"));
8
+ process.exit(1);
9
+ }
10
+ const apiUrl = options.apiUrl.replace(/\/$/, "");
11
+ process.stdout.write(chalk.gray(`\n Connecting to ${apiUrl}...`));
12
+ try {
13
+ const projects = await fetchProjects(apiUrl, options.token);
14
+ saveConfig({ apiUrl, cliToken: options.token });
15
+ console.log(chalk.green(" ✓"));
16
+ console.log(chalk.green("\n ✓ Logged in successfully"));
17
+ console.log(chalk.gray(` Found ${projects.length} project(s)`));
18
+ projects.slice(0, 3).forEach((p) => {
19
+ console.log(chalk.gray(` • ${p.name} (${p.url})`));
20
+ });
21
+ if (projects.length > 3) {
22
+ console.log(chalk.gray(` ... and ${projects.length - 3} more`));
23
+ }
24
+ console.log(chalk.gray("\n Run: ") + chalk.white("qazen record") + chalk.gray(" to capture a session\n"));
25
+ }
26
+ catch (err) {
27
+ console.log(chalk.red(" ✗"));
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ console.log(chalk.red(`\n Error: ${msg}\n`));
30
+ process.exit(1);
31
+ }
32
+ }
@@ -0,0 +1,107 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import inquirer from "inquirer";
4
+ import { getConfig, isConfigured } from "../lib/config.js";
5
+ import { fetchProjects, uploadSession } from "../lib/api.js";
6
+ import { recordSession } from "../lib/recorder.js";
7
+ export async function recordCommand(options) {
8
+ if (!isConfigured()) {
9
+ console.log(chalk.red("\n Not logged in."));
10
+ console.log(chalk.gray(" Run: qazen login --token <key>\n"));
11
+ process.exit(1);
12
+ }
13
+ const config = getConfig();
14
+ const apiUrl = config.apiUrl;
15
+ const cliToken = config.cliToken;
16
+ const spinner = ora("Loading projects...").start();
17
+ let projects;
18
+ try {
19
+ projects = await fetchProjects(apiUrl, cliToken);
20
+ spinner.stop();
21
+ }
22
+ catch (err) {
23
+ spinner.fail(chalk.red("Failed to load projects"));
24
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
25
+ process.exit(1);
26
+ }
27
+ if (projects.length === 0) {
28
+ console.log(chalk.yellow("\n No projects found."));
29
+ console.log(chalk.gray(` Create one at ${apiUrl}\n`));
30
+ process.exit(1);
31
+ }
32
+ let project;
33
+ if (options.project) {
34
+ const needle = options.project.toLowerCase();
35
+ const found = projects.find((p) => p.name.toLowerCase().includes(needle) || String(p.id) === options.project);
36
+ if (!found) {
37
+ console.log(chalk.red(`\n Project "${options.project}" not found\n`));
38
+ process.exit(1);
39
+ }
40
+ project = found;
41
+ }
42
+ else if (projects.length === 1) {
43
+ project = projects[0];
44
+ console.log(chalk.gray(`\n Using project: ${project.name}`));
45
+ }
46
+ else {
47
+ const { projectId } = (await inquirer.prompt([
48
+ {
49
+ type: "list",
50
+ name: "projectId",
51
+ message: "Select project:",
52
+ choices: projects.map((p) => ({
53
+ name: `${p.name} ${chalk.gray(p.url)} ${chalk.dim("[" + p.environment + "]")}`,
54
+ value: p.id,
55
+ })),
56
+ },
57
+ ]));
58
+ project = projects.find((p) => p.id === projectId);
59
+ }
60
+ const targetUrl = options.url || project.url;
61
+ console.log("\n" + chalk.hex("#6366F1").bold(" QAZen Session Recorder"));
62
+ console.log(chalk.gray(" ──────────────────────────────────"));
63
+ console.log(` ${chalk.gray("Project:")} ${chalk.white(project.name)}`);
64
+ console.log(` ${chalk.gray("URL:")} ${chalk.white(targetUrl)}`);
65
+ console.log(chalk.gray(" ──────────────────────────────────"));
66
+ console.log(chalk.yellow("\n A browser window will open."));
67
+ console.log(chalk.white(" • Complete your full login flow"));
68
+ console.log(chalk.white(" • SSO, ADFS, Okta, MFA — all supported"));
69
+ console.log(chalk.white(" • When you see your dashboard,"));
70
+ console.log(chalk.hex("#6366F1").bold(" press [Enter] here") + chalk.white(" to capture\n"));
71
+ const { confirm } = (await inquirer.prompt([
72
+ { type: "confirm", name: "confirm", message: "Open browser and start recording?", default: true },
73
+ ]));
74
+ if (!confirm) {
75
+ console.log(chalk.gray("\n Cancelled.\n"));
76
+ process.exit(0);
77
+ }
78
+ let result;
79
+ try {
80
+ result = await recordSession(targetUrl, () => {
81
+ console.log(chalk.gray("\n Browser opened. Complete your login..."));
82
+ });
83
+ }
84
+ catch (err) {
85
+ console.log(chalk.red("\n ✗ Recording failed"));
86
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
87
+ process.exit(1);
88
+ }
89
+ console.log(chalk.green(`\n ✓ Session captured`));
90
+ console.log(chalk.gray(` Cookies: ${result.cookieCount} (including HttpOnly)`));
91
+ console.log(chalk.gray(` Domain: ${result.domain}`));
92
+ const uploadSpinner = ora("Uploading to QAZen...").start();
93
+ try {
94
+ await uploadSession(apiUrl, cliToken, project.id, result.storageState, result.domain, result.cookieCount);
95
+ uploadSpinner.succeed(chalk.green("Session saved to QAZen"));
96
+ }
97
+ catch (err) {
98
+ uploadSpinner.fail(chalk.red("Upload failed"));
99
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
100
+ process.exit(1);
101
+ }
102
+ console.log("\n" + chalk.hex("#6366F1").bold(" ✓ Ready to test!\n"));
103
+ console.log(chalk.white(" Your session is active in QAZen."));
104
+ console.log(chalk.white(" Go to Discovery and run against:"));
105
+ console.log(chalk.hex("#6366F1")(` ${targetUrl}\n`));
106
+ console.log(chalk.gray(` ${apiUrl}/discovery\n`));
107
+ }
@@ -0,0 +1,28 @@
1
+ import chalk from "chalk";
2
+ import { getConfig, isConfigured, configPath } from "../lib/config.js";
3
+ import { fetchProjects } from "../lib/api.js";
4
+ export async function statusCommand() {
5
+ console.log("\n" + chalk.hex("#6366F1").bold(" QAZen CLI\n"));
6
+ if (!isConfigured()) {
7
+ console.log(chalk.red(" ✗ Not configured"));
8
+ console.log(chalk.gray(" Run: qazen login --token <key>\n"));
9
+ return;
10
+ }
11
+ const config = getConfig();
12
+ console.log(chalk.green(" ✓ Configured"));
13
+ console.log(chalk.gray(` API: ${config.apiUrl}`));
14
+ console.log(chalk.gray(` Config: ${configPath()}`));
15
+ try {
16
+ const projects = await fetchProjects(config.apiUrl, config.cliToken);
17
+ console.log(chalk.green(" ✓ API connected"));
18
+ console.log(chalk.gray(` Projects: ${projects.length}`));
19
+ projects.forEach((p) => {
20
+ console.log(chalk.gray(` • ${p.name} — ${p.url}`));
21
+ });
22
+ }
23
+ catch (err) {
24
+ console.log(chalk.red(" ✗ API connection failed"));
25
+ console.error(chalk.red(` ${err instanceof Error ? err.message : String(err)}`));
26
+ }
27
+ console.log();
28
+ }
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import chalk from "chalk";
4
+ import { loginCommand } from "./commands/login.js";
5
+ import { recordCommand } from "./commands/record.js";
6
+ import { statusCommand } from "./commands/status.js";
7
+ console.log(chalk.hex("#6366F1").bold("\n QAZen") + chalk.gray(" — Autonomous QA Platform\n"));
8
+ program
9
+ .name("qazen")
10
+ .description("QAZen CLI — capture authenticated sessions for enterprise app testing")
11
+ .version("0.1.0");
12
+ program
13
+ .command("login")
14
+ .description("Connect CLI to your QAZen workspace")
15
+ .requiredOption("--token <token>", "CLI API token from QAZen Settings → Authentication")
16
+ .option("--api-url <url>", "QAZen API URL", "https://qa-zen-ai.replit.app")
17
+ .action(loginCommand);
18
+ program
19
+ .command("record")
20
+ .description("Record an authenticated browser session")
21
+ .option("-p, --project <name>", "Project name or id to record for")
22
+ .option("-u, --url <url>", "Override the project URL")
23
+ .action(recordCommand);
24
+ program
25
+ .command("status")
26
+ .description("Check CLI connection and list projects")
27
+ .action(statusCommand);
28
+ program.parse();
@@ -0,0 +1,34 @@
1
+ function authHeaders(token) {
2
+ return { "x-qazen-cli-token": token };
3
+ }
4
+ export async function fetchProjects(apiUrl, cliToken) {
5
+ const res = await fetch(`${apiUrl}/api/cli/projects`, {
6
+ headers: authHeaders(cliToken),
7
+ });
8
+ if (res.status === 401) {
9
+ throw new Error("Invalid CLI token. Run: qazen login --token <key>");
10
+ }
11
+ if (!res.ok) {
12
+ throw new Error(`Failed to fetch projects (HTTP ${res.status})`);
13
+ }
14
+ return (await res.json());
15
+ }
16
+ export async function uploadSession(apiUrl, cliToken, projectId, storageState, domain, cookieCount) {
17
+ const res = await fetch(`${apiUrl}/api/cli/projects/${projectId}/session`, {
18
+ method: "PATCH",
19
+ headers: { "Content-Type": "application/json", ...authHeaders(cliToken) },
20
+ body: JSON.stringify({
21
+ storageState: JSON.stringify(storageState),
22
+ storageStateCapturedAt: new Date().toISOString(),
23
+ capturedDomain: domain,
24
+ cookieCount,
25
+ captureMethod: "cli",
26
+ }),
27
+ });
28
+ if (res.status === 401)
29
+ throw new Error("Invalid CLI token");
30
+ if (!res.ok) {
31
+ const text = await res.text().catch(() => "");
32
+ throw new Error(`Upload failed (HTTP ${res.status}): ${text}`);
33
+ }
34
+ }
@@ -0,0 +1,23 @@
1
+ import Conf from "conf";
2
+ const conf = new Conf({ projectName: "qazen-cli" });
3
+ export function getConfig() {
4
+ return {
5
+ apiUrl: conf.get("apiUrl"),
6
+ cliToken: conf.get("cliToken"),
7
+ };
8
+ }
9
+ export function saveConfig(values) {
10
+ if (values.apiUrl)
11
+ conf.set("apiUrl", values.apiUrl);
12
+ if (values.cliToken)
13
+ conf.set("cliToken", values.cliToken);
14
+ }
15
+ export function clearConfig() {
16
+ conf.clear();
17
+ }
18
+ export function isConfigured() {
19
+ return Boolean(conf.get("apiUrl") && conf.get("cliToken"));
20
+ }
21
+ export function configPath() {
22
+ return conf.path;
23
+ }
@@ -0,0 +1,62 @@
1
+ import { chromium } from "playwright";
2
+ import * as readline from "node:readline";
3
+ /**
4
+ * Launch a real, headed Chromium and let the user complete any
5
+ * login flow (SSO, ADFS, Okta, MFA, …). When the user presses
6
+ * Enter in the terminal we snapshot the full storage state via
7
+ * CDP, which includes HttpOnly cookies that JavaScript cannot
8
+ * read — this is the reason the CLI exists.
9
+ */
10
+ export async function recordSession(url, onReady) {
11
+ const browser = await chromium.launch({
12
+ headless: false,
13
+ args: [
14
+ "--no-sandbox",
15
+ "--disable-infobars",
16
+ "--start-maximized",
17
+ "--disable-blink-features=AutomationControlled",
18
+ ],
19
+ ignoreDefaultArgs: ["--enable-automation"],
20
+ });
21
+ const context = await browser.newContext({
22
+ viewport: null,
23
+ ignoreHTTPSErrors: true,
24
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
25
+ });
26
+ const page = await context.newPage();
27
+ try {
28
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
29
+ }
30
+ catch {
31
+ // Some apps redirect to a different origin immediately; that's fine — keep going.
32
+ }
33
+ onReady();
34
+ await waitForEnter();
35
+ const storageState = await context.storageState();
36
+ const cookieCount = storageState.cookies?.length ?? 0;
37
+ let domain = "unknown";
38
+ try {
39
+ domain = new URL(page.url()).hostname;
40
+ }
41
+ catch {
42
+ try {
43
+ domain = new URL(url).hostname;
44
+ }
45
+ catch {
46
+ /* leave as "unknown" */
47
+ }
48
+ }
49
+ await browser.close();
50
+ return { storageState, cookieCount, domain };
51
+ }
52
+ function waitForEnter() {
53
+ return new Promise((resolve) => {
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
55
+ process.stdout.write("\n \x1b[33m→ Complete your login, then press [Enter] \x1b[0m");
56
+ rl.once("line", () => {
57
+ rl.close();
58
+ process.stdout.write("\n");
59
+ resolve();
60
+ });
61
+ });
62
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "qazen-cli",
3
+ "version": "0.1.0",
4
+ "description": "QAZen CLI — capture authenticated browser sessions for enterprise SSO testing",
5
+ "license": "MIT",
6
+ "author": "QAZen",
7
+ "homepage": "https://qa-zen-ai.replit.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/akandula0309/qazen-cli.git"
11
+ },
12
+ "keywords": [
13
+ "qa",
14
+ "testing",
15
+ "automation",
16
+ "playwright",
17
+ "sso",
18
+ "enterprise"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "qazen": "dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc && chmod +x dist/index.js",
30
+ "dev": "tsx src/index.ts",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepublishOnly": "pnpm run build"
33
+ },
34
+ "dependencies": {
35
+ "chalk": "^5.3.0",
36
+ "commander": "^12.1.0",
37
+ "conf": "^13.0.0",
38
+ "inquirer": "^9.2.0",
39
+ "ora": "^8.0.0",
40
+ "playwright": "^1.60.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/inquirer": "^9.0.7",
44
+ "@types/node": "catalog:",
45
+ "tsx": "catalog:",
46
+ "typescript": "^5.9.0"
47
+ }
48
+ }