ghostrun-cli 1.0.0 → 1.0.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/REFERENCE.md ADDED
@@ -0,0 +1,165 @@
1
+ # GhostRun — Flow Actions Reference
2
+
3
+ Complete reference for all actions you can use in recorded or hand-crafted `.flow.json` files.
4
+
5
+ ---
6
+
7
+ ## Browser Actions
8
+
9
+ ### Navigation
10
+
11
+ | Action | Fields | Description |
12
+ |--------|--------|-------------|
13
+ | `navigate` | `url` | Go to URL |
14
+ | `reload` | — | Reload the current page |
15
+ | `back` | — | Browser back |
16
+ | `forward` | — | Browser forward |
17
+
18
+ ### Interaction
19
+
20
+ | Action | Fields | Description |
21
+ |--------|--------|-------------|
22
+ | `click` | `selector` | Left-click an element |
23
+ | `dblclick` | `selector` | Double-click an element |
24
+ | `fill` | `selector`, `value` | Clear field and type value |
25
+ | `type` | `selector`, `value`, `delay?` | Type with configurable key delay (ms) |
26
+ | `clear` | `selector` | Clear a field |
27
+ | `select` | `selector`, `value` | Select a dropdown option by value |
28
+ | `check` | `selector`, `value: "true"\|"false"` | Check/uncheck a checkbox |
29
+ | `focus` | `selector` | Focus an element |
30
+ | `hover` | `selector` | Mouse hover |
31
+ | `drag` | `selector`, `targetSelector` | Drag one element to another |
32
+ | `keyboard` | `key`, `selector?` | Press a key (e.g. `Enter`, `Tab`, `Control+a`) |
33
+ | `upload` | `selector`, `value` | Set file input (comma-separated paths) |
34
+
35
+ ### Waiting
36
+
37
+ | Action | Fields | Description |
38
+ |--------|--------|-------------|
39
+ | `wait` | `selector` | Wait for element to appear |
40
+ | `wait:text` | `selector`, `value` | Wait until element contains text |
41
+ | `wait:url` | `value` | Wait for URL to match pattern |
42
+ | `wait:ms` | `value` | Wait for N milliseconds |
43
+
44
+ ### Scrolling
45
+
46
+ | Action | Fields | Description |
47
+ |--------|--------|-------------|
48
+ | `scroll` | `selector?` | Scroll to element (or page) |
49
+ | `scroll:element` | `selector` | Scroll element into view |
50
+ | `scroll:bottom` | — | Scroll to bottom of page |
51
+ | `scroll:load` | `value?` | Scroll to bottom, wait for load (repeat N times) |
52
+ | `next:page` | `selector?` | Click next page link and wait |
53
+
54
+ ### Assertions
55
+
56
+ | Action | Fields | Description |
57
+ |--------|--------|-------------|
58
+ | `assert:visible` | `selector` | Assert element is visible |
59
+ | `assert:hidden` | `selector` | Assert element is not visible |
60
+ | `assert:text` | `selector`, `value` | Assert element contains text |
61
+ | `assert:not-text` | `selector`, `value` | Assert element does NOT contain text |
62
+ | `assert:value` | `selector`, `value` | Assert input value |
63
+ | `assert:count` | `selector`, `value` | Assert number of matching elements |
64
+ | `assert:attr` | `selector`, `value: "attr=expected"` | Assert element attribute |
65
+
66
+ ### Data Extraction
67
+
68
+ | Action | Fields | Description |
69
+ |--------|--------|-------------|
70
+ | `extract` | `selector`, `variable: "variableName"` | Extract element text → variable |
71
+ | `screenshot` | — | Capture screenshot at this step |
72
+
73
+ ### Browser State
74
+
75
+ | Action | Fields | Description |
76
+ |--------|--------|-------------|
77
+ | `cookie:set` | `value: "name=value; domain=..."` | Set a cookie |
78
+ | `cookie:clear` | — | Clear all cookies |
79
+ | `storage:set` | `selector: "key"`, `value: "val"` | Set localStorage item |
80
+ | `eval` | `value` | Execute JavaScript on the page |
81
+ | `iframe:enter` | `selector` | Enter an iframe context |
82
+ | `iframe:exit` | — | Exit iframe context, return to main frame |
83
+
84
+ ---
85
+
86
+ ## API Actions
87
+
88
+ ### HTTP Requests
89
+
90
+ | Action | Fields | Description |
91
+ |--------|--------|-------------|
92
+ | `http:request` | `method`, `url`, `headers?`, `body?`, `auth?`, `extract?` | Make an HTTP request. `auth` supports `{ type: "bearer", token: "{{var}}" }`. `extract` is a map of `variableName → $.jsonPath`. |
93
+
94
+ ### Assertions
95
+
96
+ | Action | Fields | Description |
97
+ |--------|--------|-------------|
98
+ | `assert:response` | `assert: "status"`, `expected` | Assert HTTP status code |
99
+ | `assert:response` | `assert: "json:path"`, `path`, `expected` | Assert JSONPath value equals expected |
100
+ | `assert:response` | `assert: "json:exists"`, `path` | Assert JSONPath exists in response |
101
+ | `assert:response` | `assert: "header"`, `header`, `expected` | Assert response header value |
102
+ | `assert:response` | `assert: "body:contains"`, `expected` | Assert raw body contains string |
103
+ | `assert:response` | `assert: "time"`, `expected` | Assert response time < expected ms |
104
+
105
+ ### Variables & Flow Control
106
+
107
+ | Action | Fields | Description |
108
+ |--------|--------|-------------|
109
+ | `set:variable` | `variable`, `value` | Set a named variable (supports `{{interpolation}}`) |
110
+ | `extract:json` | `variable`, `path` | Extract a value from the last response body via JSONPath |
111
+ | `env:switch` | `value` | Switch active environment mid-flow |
112
+
113
+ ---
114
+
115
+ ## Variables
116
+
117
+ Use `{{variableName}}` in any `value`, `url`, `selector`, or `body` field:
118
+
119
+ ```json
120
+ { "action": "fill", "selector": "#email", "value": "{{userEmail}}" }
121
+ ```
122
+
123
+ Pass at runtime:
124
+
125
+ ```bash
126
+ ghostrun run <id> --var userEmail=user@example.com
127
+ ```
128
+
129
+ Values extracted with `extract:` and `extract:json` are automatically available as variables in subsequent steps.
130
+
131
+ ---
132
+
133
+ ## Limitations
134
+
135
+ | Interaction | Status | Notes |
136
+ |------------|--------|-------|
137
+ | Canvas drawing | ❌ | `<canvas>` elements — no visual capture |
138
+ | WebGL / Three.js | ❌ | GPU-rendered content |
139
+ | Browser native dialogs | ⚠️ Partial | `alert()`/`confirm()`/`prompt()` auto-dismissed |
140
+ | File download verification | ⚠️ Partial | Download triggers but content is not validated |
141
+ | WebRTC / media streams | ❌ | Camera, mic, screen capture APIs |
142
+ | Browser extensions | ❌ | Extension UI not accessible via Playwright |
143
+ | Shadow DOM (closed mode) | ⚠️ Limited | Open shadow DOM works; closed mode needs `eval` workaround |
144
+ | Multi-tab / popup flows | ⚠️ Partial | New tabs opened by click are not automatically followed |
145
+ | OS-level dialogs | ❌ | Native file picker, print dialog, OS auth prompts |
146
+ | CAPTCHAs | ❌ | By design — no circumvention |
147
+ | Biometric auth | ❌ | Touch ID, Face ID, WebAuthn |
148
+ | Browser gestures (pinch/zoom) | ❌ | Mobile multi-touch gestures |
149
+ | Hover-only menus (CSS `:hover`) | ✅ | Use `hover` action before clicking submenu items |
150
+ | Right-click context menus | ⚠️ Limited | Browser context menus inaccessible; app-level menus often work |
151
+ | Drag and drop | ✅ | Use `drag` with `selector` + `targetSelector` |
152
+ | Infinite scroll / lazy load | ✅ | Use `scroll:load` with repeat count |
153
+
154
+ **Workarounds:**
155
+
156
+ ```json
157
+ // Run JS directly
158
+ { "action": "eval", "value": "document.querySelector('#btn').click()" }
159
+
160
+ // Shadow DOM
161
+ { "action": "eval", "value": "document.querySelector('my-el').shadowRoot.querySelector('button').click()" }
162
+
163
+ // Timing-sensitive steps
164
+ { "action": "wait:ms", "value": "500" }
165
+ ```
package/ghostrun.js CHANGED
@@ -3644,7 +3644,7 @@ var import_uuid = require("uuid");
3644
3644
  var HOME_DIR = process.env.HOME || process.env.USERPROFILE || ".";
3645
3645
  var DATA_PATH = path.join(HOME_DIR, ".ghostrun");
3646
3646
  var DB_PATH = path.join(DATA_PATH, "data", "ghostrun.db");
3647
- var DatabaseManager = class {
3647
+ var DatabaseManager = class _DatabaseManager {
3648
3648
  db;
3649
3649
  constructor() {
3650
3650
  fs.mkdirSync(path.join(DATA_PATH, "data"), { recursive: true });
@@ -3982,35 +3982,72 @@ var DatabaseManager = class {
3982
3982
  };
3983
3983
  }
3984
3984
  // ---- DB migrations ----
3985
+ //
3986
+ // Uses SQLite's built-in PRAGMA user_version as a schema version counter.
3987
+ // Each migration runs exactly once: we read the current version, apply every
3988
+ // migration whose index is >= that version (in order), then write the new version.
3989
+ //
3990
+ // HOW TO ADD A NEW MIGRATION:
3991
+ // 1. Append a new string to the MIGRATIONS array below.
3992
+ // 2. That's it. The runner handles the rest.
3993
+ //
3994
+ // Never edit or reorder existing entries — just append.
3995
+ static MIGRATIONS = [
3996
+ // v1: add diff_percent to steps
3997
+ "ALTER TABLE steps ADD COLUMN diff_percent REAL",
3998
+ // v2: add created_by to flows
3999
+ "ALTER TABLE flows ADD COLUMN created_by TEXT NOT NULL DEFAULT 'human'",
4000
+ // v3: add verified flag to flows
4001
+ "ALTER TABLE flows ADD COLUMN verified INTEGER NOT NULL DEFAULT 0",
4002
+ // v4: add captured_at to run_data
4003
+ "ALTER TABLE run_data ADD COLUMN captured_at TEXT DEFAULT (datetime('now'))",
4004
+ // v5: environments table
4005
+ `CREATE TABLE IF NOT EXISTS environments (
4006
+ id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, base_url TEXT,
4007
+ variables TEXT NOT NULL DEFAULT '{}', is_active INTEGER NOT NULL DEFAULT 0,
4008
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
4009
+ )`,
4010
+ // v6: api_responses table
4011
+ `CREATE TABLE IF NOT EXISTS api_responses (
4012
+ id TEXT PRIMARY KEY, run_id TEXT NOT NULL, step_number INTEGER NOT NULL,
4013
+ method TEXT NOT NULL, url TEXT NOT NULL, status_code INTEGER,
4014
+ response_time_ms INTEGER, response_headers TEXT, response_body TEXT,
4015
+ error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now'))
4016
+ )`,
4017
+ // v7: perf_runs table
4018
+ `CREATE TABLE IF NOT EXISTS perf_runs (
4019
+ id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, flow_name TEXT NOT NULL,
4020
+ config TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running',
4021
+ total_requests INTEGER, success_requests INTEGER, failed_requests INTEGER,
4022
+ avg_rps REAL, p50_ms INTEGER, p95_ms INTEGER, p99_ms INTEGER,
4023
+ min_ms INTEGER, max_ms INTEGER, per_step_stats TEXT,
4024
+ started_at TEXT NOT NULL DEFAULT (datetime('now')), completed_at TEXT
4025
+ )`
4026
+ // --- add new migrations below this line ---
4027
+ ];
4028
+ // Number of migrations that existed before we introduced versioning.
4029
+ // Existing databases have these applied already (via old try/catch approach)
4030
+ // but their user_version is 0. We detect this and fast-forward rather than
4031
+ // re-running them (which would throw "duplicate column" errors).
4032
+ static LEGACY_MIGRATION_COUNT = 7;
4033
+ columnExists(table, column) {
4034
+ const cols = this.db.pragma(`table_info(${table})`);
4035
+ return cols.some((c) => c.name === column);
4036
+ }
3985
4037
  runMigrations() {
3986
- try {
3987
- this.db.exec("ALTER TABLE steps ADD COLUMN diff_percent REAL");
3988
- } catch {
3989
- }
3990
- try {
3991
- this.db.exec("ALTER TABLE flows ADD COLUMN created_by TEXT NOT NULL DEFAULT 'human'");
3992
- } catch {
3993
- }
3994
- try {
3995
- this.db.exec("ALTER TABLE flows ADD COLUMN verified INTEGER NOT NULL DEFAULT 0");
3996
- } catch {
3997
- }
3998
- try {
3999
- this.db.exec("ALTER TABLE run_data ADD COLUMN captured_at TEXT DEFAULT (datetime('now'))");
4000
- } catch {
4001
- }
4002
- try {
4003
- this.db.exec(`CREATE TABLE IF NOT EXISTS environments (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, base_url TEXT, variables TEXT NOT NULL DEFAULT '{}', is_active INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
4004
- } catch {
4005
- }
4006
- try {
4007
- this.db.exec(`CREATE TABLE IF NOT EXISTS api_responses (id TEXT PRIMARY KEY, run_id TEXT NOT NULL, step_number INTEGER NOT NULL, method TEXT NOT NULL, url TEXT NOT NULL, status_code INTEGER, response_time_ms INTEGER, response_headers TEXT, response_body TEXT, error_message TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')))`);
4008
- } catch {
4009
- }
4010
- try {
4011
- this.db.exec(`CREATE TABLE IF NOT EXISTS perf_runs (id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, flow_name TEXT NOT NULL, config TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', total_requests INTEGER, success_requests INTEGER, failed_requests INTEGER, avg_rps REAL, p50_ms INTEGER, p95_ms INTEGER, p99_ms INTEGER, min_ms INTEGER, max_ms INTEGER, per_step_stats TEXT, started_at TEXT NOT NULL DEFAULT (datetime('now')), completed_at TEXT)`);
4012
- } catch {
4038
+ let currentVersion = this.db.pragma("user_version", { simple: true }) ?? 0;
4039
+ if (currentVersion === 0 && this.columnExists("steps", "diff_percent")) {
4040
+ currentVersion = _DatabaseManager.LEGACY_MIGRATION_COUNT;
4041
+ this.db.pragma(`user_version = ${currentVersion}`);
4013
4042
  }
4043
+ if (currentVersion >= _DatabaseManager.MIGRATIONS.length) return;
4044
+ const applyAll = this.db.transaction(() => {
4045
+ for (let i = currentVersion; i < _DatabaseManager.MIGRATIONS.length; i++) {
4046
+ this.db.exec(_DatabaseManager.MIGRATIONS[i]);
4047
+ }
4048
+ this.db.pragma(`user_version = ${_DatabaseManager.MIGRATIONS.length}`);
4049
+ });
4050
+ applyAll();
4014
4051
  }
4015
4052
  // ---- Suites ----
4016
4053
  createSuite(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostrun-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Browser automation + API testing + load testing in one tool. Record flows, test REST APIs, run VU-based load tests, export to k6. Entirely local.",
5
5
  "main": "ghostrun.js",
6
6
  "bin": {
@@ -8,13 +8,21 @@
8
8
  "ghostrun-mcp": "mcp-server.js"
9
9
  },
10
10
  "keywords": [
11
- "browser-automation", "api-testing", "load-testing", "playwright",
12
- "test-runner", "cli", "k6", "local-first", "mcp", "e2e-testing"
11
+ "browser-automation",
12
+ "api-testing",
13
+ "load-testing",
14
+ "playwright",
15
+ "test-runner",
16
+ "cli",
17
+ "k6",
18
+ "local-first",
19
+ "mcp",
20
+ "e2e-testing"
13
21
  ],
14
22
  "homepage": "https://ghostrun.builtbysharan.com",
15
23
  "repository": {
16
24
  "type": "git",
17
- "url": "https://github.com/builtbysharan/ghostrun"
25
+ "url": "https://github.com/TechBuiltBySharan/ghostrun"
18
26
  },
19
27
  "license": "MIT",
20
28
  "scripts": {