@qatonic_innovations/qaios 0.1.1 → 0.1.2

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 CHANGED
@@ -10,6 +10,18 @@ QAIOS acts like a QA engineer on your team. Point it at a feature description or
10
10
 
11
11
  **Mental model: Claude Code, but for QA work instead of feature coding.**
12
12
 
13
+ > **Status: early alpha (v0.1).** Core flows — `init`, `doctor`, test design
14
+ > & generation, Playwright execution, self-healing, accessibility, and the
15
+ > audit log — work today. Expect rough edges and please
16
+ > [report issues](https://github.com/qatonic/qaios/issues). After installing,
17
+ > a quick smoke test confirms everything resolves:
18
+ >
19
+ > ```bash
20
+ > qaios --version
21
+ > qaios --help
22
+ > qaios doctor # checks Node, API key, Playwright, config
23
+ > ```
24
+
13
25
  ---
14
26
 
15
27
  ## Install
@@ -23,18 +35,39 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
23
35
 
24
36
  ### Requirements
25
37
 
26
- - **Node.js 20 LTS or newer**
27
- - An **Anthropic API key** in your environment:
38
+ - **Node.js 20 LTS recommended.** QAIOS bundles a native SQLite module
39
+ (better-sqlite3) for its local audit log; Node 20 LTS has the widest
40
+ prebuilt-binary coverage. Newer Node usually works but may need to compile
41
+ the binary (build tools + network access).
42
+ - An **Anthropic API key** — get one at
43
+ [console.anthropic.com](https://console.anthropic.com/settings/keys), then
44
+ put it in your environment:
28
45
  ```bash
29
46
  export ANTHROPIC_API_KEY=sk-ant-... # macOS/Linux
30
- setx ANTHROPIC_API_KEY "sk-ant-..." # Windows (new shell after)
47
+ setx ANTHROPIC_API_KEY "sk-ant-..." # Windows (open a NEW shell after)
31
48
  ```
32
- - **Playwright** in your project, for `qaios run` / `snapshot` / `a11y`:
49
+ The key is read from the environment and **never written to disk** by QAIOS.
50
+ - **Playwright** in your project, for `qaios run` / `snapshot` / `explore` / `a11y`:
33
51
  ```bash
34
52
  npm i -D @playwright/test && npx playwright install
35
53
  ```
54
+ (`@playwright/test` is for `run`; `explore`/`a11y` use the `playwright`
55
+ package, which it pulls in.)
36
56
  - For `qaios a11y`, also: `npm i -D @axe-core/playwright`
37
57
 
58
+ ### Install troubleshooting
59
+
60
+ If `qaios init` fails with a SQLite/native-binding error
61
+ (`Could not load the native SQLite module`):
62
+
63
+ - Confirm your Node version: `node -v` (prefer 20 LTS).
64
+ - Rebuild the binary: `npm rebuild better-sqlite3` — or reinstall qaios.
65
+ - Behind a proxy/firewall? The prebuilt binary is fetched from GitHub
66
+ release assets; allow that host, or install C/C++ build tools so it can
67
+ compile locally.
68
+
69
+ `qaios doctor` will tell you exactly which check failed and what to run.
70
+
38
71
  ---
39
72
 
40
73
  ## 60-second quick start
@@ -158,7 +191,26 @@ qaios config set mode TRUST
158
191
  ## Cost & privacy
159
192
 
160
193
  - All v0.1 skills use Claude Sonnet. A typical `qaios test` costs ~**$0.04–0.10**. Each workflow is capped (default `min(15 calls, $0.50)`, configurable) and aborts if exceeded.
161
- - **Local-first.** No telemetry, no phone-home. The only outbound traffic is (a) LLM calls to Anthropic with the prompts QAIOS builds, and (b) any MCP servers you configure. You bring your own API key; QAIOS does not proxy your requests.
194
+ - **Local-first.** No telemetry, no phone-home. The only outbound traffic is (a) LLM calls to Anthropic with the prompts QAIOS builds, and (b) any MCP servers you configure. You bring your own API key; QAIOS does not proxy your requests. See [SECURITY.md](https://github.com/qatonic/qaios/blob/main/SECURITY.md) for exactly what's sent.
195
+
196
+ ---
197
+
198
+ ## Exit codes (for CI)
199
+
200
+ Every command exits with a stable, documented code so you can gate pipelines:
201
+
202
+ | Code | Meaning | Example |
203
+ | ----- | ------------------------------- | ----------------------------------------------------------------------- |
204
+ | `0` | Success | tests generated; `doctor` all-green; `a11y` clean |
205
+ | `1` | User error / actionable failure | bad flag; `a11y` found real violations; `run` had failing tests |
206
+ | `2` | Gate blocked (informational) | a workflow paused for human review — run `qaios review` or pass `--yes` |
207
+ | `3` | Tool/dependency error | Playwright or axe not installed; SQLite binding missing |
208
+ | `4` | LLM error | rate limit, timeout, or invalid API key |
209
+ | `5` | Internal error | unexpected crash (re-run with `--debug` for a stack trace) |
210
+ | `130` | Cancelled | you pressed Ctrl+C |
211
+
212
+ `qaios doctor --json` and `--json` on most commands emit machine-readable
213
+ output for CI consumption.
162
214
 
163
215
  ---
164
216
 
@@ -19,6 +19,26 @@
19
19
  // 0 = success (even when violations were found)
20
20
 
21
21
  import { writeFileSync } from 'node:fs';
22
+ import { createRequire } from 'node:module';
23
+ import path from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
+
26
+ // Resolve a package from the USER's project (process.cwd()), not from
27
+ // where this script physically lives (inside QAIOS's install dir). With a
28
+ // bare `import('playwright')`, Node resolves relative to THIS file — so a
29
+ // globally-installed QAIOS can't see the user's locally-installed
30
+ // playwright. Resolving via a require rooted at the project's package.json
31
+ // (falling back to cwd) and importing the resolved file URL fixes that.
32
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
33
+ // Resolve from the user's project, but import the BARE specifier so Node
34
+ // honors the package's `exports` map (giving the proper ESM shape). We only
35
+ // use require.resolve to (a) verify the package exists in the project and
36
+ // (b) make the bare import resolvable, by importing the resolved entry and
37
+ // reading a named property off it. Returns the live module namespace.
38
+ const importFromProject = async (specifier) => {
39
+ const resolved = projectRequire.resolve(specifier); // throws if not installed
40
+ return import(pathToFileURL(resolved).href);
41
+ };
22
42
 
23
43
  const url = process.env.QAIOS_AXE_URL;
24
44
  const out = process.env.QAIOS_AXE_OUTPUT;
@@ -36,8 +56,14 @@ if (!url || !out) {
36
56
 
37
57
  let chromium, AxeBuilder;
38
58
  try {
39
- ({ chromium } = await import('playwright'));
40
- ({ default: AxeBuilder } = await import('@axe-core/playwright'));
59
+ // Importing a CJS entry via file URL nests the package's exports under
60
+ // `default`; importing an ESM entry exposes them as named. Read from
61
+ // whichever is present so both layouts work.
62
+ const pw = await importFromProject('playwright');
63
+ chromium = pw.chromium ?? pw.default?.chromium;
64
+ const axe = await importFromProject('@axe-core/playwright');
65
+ AxeBuilder = axe.default ?? axe.AxeBuilder;
66
+ if (!chromium || !AxeBuilder) throw new Error('resolved module missing expected export');
41
67
  } catch (err) {
42
68
  process.stderr.write(
43
69
  `axe-runner: failed to load 'playwright' and '@axe-core/playwright' from the project — ` +
@@ -12,6 +12,20 @@
12
12
  // via truncateHtmlSafe so the script stays decoupled from the budget.
13
13
 
14
14
  import { writeFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+
19
+ // Resolve `playwright` from the USER's project (process.cwd()), not from
20
+ // where this script lives inside QAIOS's install dir. A bare
21
+ // `import('playwright')` resolves relative to THIS file, so a globally
22
+ // installed QAIOS can't see the user's local playwright. See axe-runner.mjs
23
+ // for the full rationale.
24
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
25
+ const importFromProject = async (specifier) => {
26
+ const resolved = projectRequire.resolve(specifier); // throws if not installed
27
+ return import(pathToFileURL(resolved).href);
28
+ };
15
29
 
16
30
  const url = process.env.QAIOS_CAPTURE_URL;
17
31
  const out = process.env.QAIOS_CAPTURE_OUTPUT;
@@ -27,7 +41,9 @@ if (!url || !out) {
27
41
 
28
42
  let chromium;
29
43
  try {
30
- ({ chromium } = await import('playwright'));
44
+ const pw = await importFromProject('playwright');
45
+ chromium = pw.chromium ?? pw.default?.chromium;
46
+ if (!chromium) throw new Error('playwright resolved but has no chromium export');
31
47
  } catch (err) {
32
48
  process.stderr.write(
33
49
  `capture-page: failed to load 'playwright' from project — is it installed? ${err?.message ?? err}\n`,
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { useApp, useInput, Box, Text } from 'ink';
3
3
  import { useState } from 'react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
- import { readFileSync, existsSync, rmSync, mkdirSync, writeFileSync, statSync, mkdtempSync, copyFileSync, readdirSync } from 'fs';
5
+ import { realpathSync, readFileSync, existsSync, rmSync, mkdirSync, writeFileSync, statSync, mkdtempSync, copyFileSync, readdirSync } from 'fs';
6
6
  import path12 from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { Command, InvalidArgumentError } from 'commander';
@@ -2708,13 +2708,14 @@ function resolveSpawn(bin, args) {
2708
2708
  }
2709
2709
  return { bin: resolved, args };
2710
2710
  }
2711
+ var SUPPRESSED_WARNING_CODES = /* @__PURE__ */ new Set(["DEP0190", "DEP0040"]);
2711
2712
  var _filterInstalled = false;
2712
2713
  function installDeprecationWarningFilter() {
2713
2714
  if (_filterInstalled) return;
2714
2715
  _filterInstalled = true;
2715
2716
  process.removeAllListeners("warning");
2716
2717
  process.on("warning", (w) => {
2717
- if (w.code === "DEP0190") return;
2718
+ if (w.code !== void 0 && SUPPRESSED_WARNING_CODES.has(w.code)) return;
2718
2719
  process.stderr.write(`(node:${process.pid}) ${w.name}: ${w.message}
2719
2720
  `);
2720
2721
  if (w.stack !== void 0) process.stderr.write(`${w.stack}
@@ -8547,6 +8548,18 @@ async function runExplore(opts) {
8547
8548
  }
8548
8549
  };
8549
8550
  }
8551
+ if (opts.duration !== void 0) {
8552
+ const d = opts.duration;
8553
+ if (!Number.isFinite(d) || !Number.isInteger(d) || d < 60) {
8554
+ return {
8555
+ exitCode: ExitCode.USER_ERROR,
8556
+ error: {
8557
+ code: "qaios.explore.invalid_duration",
8558
+ message: `--duration must be a whole number of seconds \u2265 60 (got ${String(d)}).`
8559
+ }
8560
+ };
8561
+ }
8562
+ }
8550
8563
  const config = loadConfig2(cwd);
8551
8564
  const memory = loadProjectMemory(cwd);
8552
8565
  const mode = opts.mode ?? config?.mode ?? "LITE";
@@ -9594,8 +9607,6 @@ function runInit(opts = {}) {
9594
9607
  if (existsSync(qaiosDir) && opts.force) {
9595
9608
  rmSync(qaiosDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
9596
9609
  }
9597
- mkdirSync(qaiosDir, { recursive: true });
9598
- const filesWritten = [];
9599
9610
  const modeParse = Mode.safeParse(opts.mode ?? "LITE");
9600
9611
  if (!modeParse.success) {
9601
9612
  return {
@@ -9614,9 +9625,7 @@ function runInit(opts = {}) {
9614
9625
  testDir: opts.testDir ?? detection.testDir ?? "tests"
9615
9626
  }
9616
9627
  });
9617
- const configPath = path12.join(qaiosDir, "config.yaml");
9618
- writeFileSync(configPath, stringify(config), "utf-8");
9619
- filesWritten.push(path12.relative(cwd, configPath));
9628
+ mkdirSync(qaiosDir, { recursive: true });
9620
9629
  const dbPath = path12.join(qaiosDir, "workflows.db");
9621
9630
  let migrations;
9622
9631
  try {
@@ -9624,16 +9633,33 @@ function runInit(opts = {}) {
9624
9633
  migrations = storage.runMigrations();
9625
9634
  storage.close();
9626
9635
  } catch (err) {
9636
+ try {
9637
+ rmSync(qaiosDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
9638
+ } catch {
9639
+ }
9640
+ const raw = err.message ?? String(err);
9641
+ const isBindingError = /bindings file|was compiled against|NODE_MODULE_VERSION|\.node/i.test(
9642
+ raw
9643
+ );
9644
+ const message = isBindingError ? `Could not load the native SQLite module (better-sqlite3). This usually means the prebuilt binary didn't download or doesn't match your Node version.
9645
+ \u2022 Ensure you're on Node 20 LTS (run \`node -v\`).
9646
+ \u2022 Reinstall to fetch/rebuild the binary: \`npm rebuild better-sqlite3\` (or reinstall qaios).
9647
+ \u2022 Behind a proxy/firewall? The binary is fetched from GitHub releases \u2014 allow that, or install build tools so it can compile.
9648
+ Original error: ${raw}` : `Failed to initialise workflows.db: ${raw}`;
9627
9649
  return {
9628
- exitCode: ExitCode.INTERNAL,
9650
+ exitCode: err instanceof StorageError ? ExitCode.INTERNAL : ExitCode.TOOL_ERROR,
9629
9651
  error: {
9630
- code: err instanceof StorageError ? err.code : "qaios.init.db_failed",
9631
- message: `Failed to initialise workflows.db: ${err.message}`
9652
+ code: isBindingError ? "qaios.init.sqlite_binding_missing" : err instanceof StorageError ? err.code : "qaios.init.db_failed",
9653
+ message
9632
9654
  },
9633
9655
  detection
9634
9656
  };
9635
9657
  }
9658
+ const filesWritten = [];
9636
9659
  filesWritten.push(path12.relative(cwd, dbPath));
9660
+ const configPath = path12.join(qaiosDir, "config.yaml");
9661
+ writeFileSync(configPath, stringify(config), "utf-8");
9662
+ filesWritten.push(path12.relative(cwd, configPath));
9637
9663
  const gitignorePath = path12.join(qaiosDir, ".gitignore");
9638
9664
  writeFileSync(gitignorePath, QAIOS_GITIGNORE, "utf-8");
9639
9665
  filesWritten.push(path12.relative(cwd, gitignorePath));
@@ -12015,13 +12041,19 @@ function formatDoctor(checks) {
12015
12041
  return lines.join("\n");
12016
12042
  }
12017
12043
  var invokedAsScript = (() => {
12018
- try {
12019
- const argv1 = process.argv[1];
12020
- if (!argv1) return false;
12021
- return path12.resolve(argv1) === fileURLToPath(import.meta.url);
12022
- } catch {
12023
- return false;
12024
- }
12044
+ const argv1 = process.argv[1];
12045
+ if (!argv1) return false;
12046
+ const thisFile = fileURLToPath(import.meta.url);
12047
+ const realOf = (p) => {
12048
+ try {
12049
+ return realpathSync(p);
12050
+ } catch {
12051
+ return path12.resolve(p);
12052
+ }
12053
+ };
12054
+ if (realOf(argv1) === realOf(thisFile)) return true;
12055
+ const invokedName = path12.basename(argv1).replace(/\.(js|mjs|cjs)$/, "");
12056
+ return invokedName === "qaios";
12025
12057
  })();
12026
12058
  if (invokedAsScript) {
12027
12059
  installDeprecationWarningFilter();
@@ -18,6 +18,19 @@
18
18
  // used unconditionally for stability — same input → same PNG → same SHA.
19
19
 
20
20
  import { writeFileSync } from 'node:fs';
21
+ import { createRequire } from 'node:module';
22
+ import path from 'node:path';
23
+ import { pathToFileURL } from 'node:url';
24
+
25
+ // Resolve `playwright` from the USER's project (process.cwd()), not from
26
+ // where this script lives in QAIOS's install dir. See axe-runner.mjs for
27
+ // the full rationale (a bare import resolves relative to this file, so a
28
+ // global QAIOS can't see the user's local playwright).
29
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
30
+ const importFromProject = async (specifier) => {
31
+ const resolved = projectRequire.resolve(specifier);
32
+ return import(pathToFileURL(resolved).href);
33
+ };
21
34
 
22
35
  const url = process.env.QAIOS_SCREENSHOT_URL;
23
36
  const out = process.env.QAIOS_SCREENSHOT_OUTPUT;
@@ -37,7 +50,9 @@ const fullPage = process.env.QAIOS_SCREENSHOT_FULL_PAGE !== '0';
37
50
 
38
51
  let chromium;
39
52
  try {
40
- ({ chromium } = await import('playwright'));
53
+ const pw = await importFromProject('playwright');
54
+ chromium = pw.chromium ?? pw.default?.chromium;
55
+ if (!chromium) throw new Error('playwright resolved but has no chromium export');
41
56
  } catch (err) {
42
57
  process.stderr.write(
43
58
  `capture-screenshot: failed to load 'playwright' from project — is it installed? ${err?.message ?? err}\n`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qatonic_innovations/qaios",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "AI QA engineer in your terminal — designs, writes, runs, heals, and explores tests for web UI and APIs with audit-grade traceability.",
6
6
  "license": "MIT",
@@ -65,10 +65,7 @@
65
65
  "@types/pixelmatch": "^5.2.6",
66
66
  "@types/pngjs": "^6.0.5",
67
67
  "@types/react": "^18.3.28",
68
- "ink-testing-library": "^4.0.0",
69
- "@qaios/runtime": "0.0.0",
70
- "@qaios/shared": "0.0.0",
71
- "@qaios/skills": "0.0.0"
68
+ "ink-testing-library": "^4.0.0"
72
69
  },
73
70
  "scripts": {
74
71
  "build": "tsup && node scripts/copy-templates.mjs && node scripts/copy-runtime-assets.mjs",