runhuman 0.1.0 → 1.0.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/LICENSE +15 -0
- package/README.md +591 -33
- package/dist/index.js +236 -0
- package/package.json +49 -7
- package/index.js +0 -53
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Volter AI
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/runhuman)
|
|
4
4
|
[](https://opensource.org/licenses/ISC)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> AI-orchestrated human QA testing from your terminal
|
|
7
7
|
|
|
8
8
|
## What is Runhuman?
|
|
9
9
|
|
|
@@ -15,66 +15,624 @@ Runhuman is an AI-orchestrated human QA testing service. Get real human testing
|
|
|
15
15
|
- Visual/UX testing requiring real human judgment
|
|
16
16
|
- On-demand QA without hiring/managing teams
|
|
17
17
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
If you're using AI agents (Claude, GPT, etc.), use our MCP server package:
|
|
18
|
+
## Installation
|
|
21
19
|
|
|
22
20
|
```bash
|
|
23
21
|
# Install globally
|
|
24
|
-
npm install -g
|
|
22
|
+
npm install -g runhuman
|
|
25
23
|
|
|
26
24
|
# Or use with npx
|
|
27
|
-
npx
|
|
25
|
+
npx runhuman --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1. Login with your API key
|
|
32
|
+
runhuman login
|
|
33
|
+
|
|
34
|
+
# 2. Create your first test
|
|
35
|
+
runhuman create https://myapp.com -d "Test the checkout flow"
|
|
36
|
+
|
|
37
|
+
# 3. Check the status
|
|
38
|
+
runhuman status <jobId>
|
|
39
|
+
|
|
40
|
+
# 4. Get results
|
|
41
|
+
runhuman results <jobId>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### Jobs
|
|
47
|
+
|
|
48
|
+
#### `create [url]`
|
|
49
|
+
|
|
50
|
+
Create a new QA test job.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Basic usage
|
|
54
|
+
runhuman create https://myapp.com -d "Test checkout flow"
|
|
55
|
+
|
|
56
|
+
# With template
|
|
57
|
+
runhuman create https://myapp.com --template tmpl_abc123
|
|
58
|
+
|
|
59
|
+
# Synchronous (wait for result)
|
|
60
|
+
runhuman create https://myapp.com -d "Test" --sync
|
|
61
|
+
|
|
62
|
+
# With custom schema
|
|
63
|
+
runhuman create https://myapp.com -d "Test" --schema ./schema.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Options:**
|
|
67
|
+
- `-d, --description <text>` - Test instructions for the human tester
|
|
68
|
+
- `--template <id>` - Use a pre-defined template
|
|
69
|
+
- `--duration <seconds>` - Target test duration
|
|
70
|
+
- `--screen-size <size>` - Screen size (desktop/mobile/tablet)
|
|
71
|
+
- `--schema <path>` - Path to JSON schema for structured output
|
|
72
|
+
- `--sync` - Wait for result before exiting
|
|
73
|
+
- `--json` - Output as JSON
|
|
74
|
+
|
|
75
|
+
#### `status <jobId>`
|
|
76
|
+
|
|
77
|
+
Check the status of a test job.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
runhuman status job_abc123
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `wait <jobId>`
|
|
84
|
+
|
|
85
|
+
Wait for a job to complete and display results.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
runhuman wait job_abc123
|
|
89
|
+
runhuman wait job_abc123 --timeout 300
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Options:**
|
|
93
|
+
- `--timeout <seconds>` - Maximum wait time (default: 600)
|
|
94
|
+
|
|
95
|
+
#### `results <jobId>`
|
|
96
|
+
|
|
97
|
+
Display detailed test results.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
runhuman results job_abc123
|
|
101
|
+
runhuman results job_abc123 --json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `list`
|
|
105
|
+
|
|
106
|
+
List all your test jobs.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# List all jobs
|
|
110
|
+
runhuman list
|
|
111
|
+
|
|
112
|
+
# Filter by status
|
|
113
|
+
runhuman list --status completed
|
|
114
|
+
|
|
115
|
+
# Filter by project
|
|
116
|
+
runhuman list --project proj_abc123
|
|
117
|
+
|
|
118
|
+
# Limit results
|
|
119
|
+
runhuman list --limit 20
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Options:**
|
|
123
|
+
- `--status <status>` - Filter by status (pending/claimed/in_progress/completed/failed)
|
|
124
|
+
- `--project <id>` - Filter by project
|
|
125
|
+
- `--limit <number>` - Maximum number of jobs to return
|
|
126
|
+
- `--json` - Output as JSON
|
|
127
|
+
|
|
128
|
+
### Projects
|
|
129
|
+
|
|
130
|
+
#### `projects list`
|
|
131
|
+
|
|
132
|
+
List all your projects.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
runhuman projects list
|
|
136
|
+
runhuman projects ls # alias
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `projects create <name>`
|
|
140
|
+
|
|
141
|
+
Create a new project.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
runhuman projects create "My App"
|
|
145
|
+
runhuman projects create "My App" -d "Production QA testing"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Options:**
|
|
149
|
+
- `-d, --description <text>` - Project description
|
|
150
|
+
- `--default-url <url>` - Default URL to test
|
|
151
|
+
- `--github-repo <owner/repo>` - Link GitHub repository
|
|
152
|
+
|
|
153
|
+
#### `projects show <projectId>`
|
|
154
|
+
|
|
155
|
+
Show details of a specific project.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
runhuman projects show proj_abc123
|
|
159
|
+
runhuman projects info proj_abc123 # alias
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `projects update <projectId>`
|
|
163
|
+
|
|
164
|
+
Update a project.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
runhuman projects update proj_abc123 --name "New Name"
|
|
168
|
+
runhuman projects update proj_abc123 --description "New description"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Options:**
|
|
172
|
+
- `--name <name>` - New project name
|
|
173
|
+
- `-d, --description <text>` - New description
|
|
174
|
+
- `--default-url <url>` - New default URL
|
|
175
|
+
- `--github-repo <owner/repo>` - New GitHub repository
|
|
176
|
+
|
|
177
|
+
#### `projects delete <projectId>`
|
|
178
|
+
|
|
179
|
+
Delete a project permanently.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
runhuman projects delete proj_abc123 --force
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Options:**
|
|
186
|
+
- `--force` - Skip confirmation prompt
|
|
187
|
+
|
|
188
|
+
### API Keys
|
|
189
|
+
|
|
190
|
+
#### `keys list`
|
|
191
|
+
|
|
192
|
+
List all API keys for a project.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
runhuman keys list --project proj_abc123
|
|
196
|
+
runhuman keys ls --project proj_abc123 # alias
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Options:**
|
|
200
|
+
- `--project <id>` - Project ID (required)
|
|
201
|
+
- `--show-keys` - Show full API keys (default: masked)
|
|
202
|
+
|
|
203
|
+
#### `keys create <name>`
|
|
204
|
+
|
|
205
|
+
Create a new API key.
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
runhuman keys create "CI/CD Key" --project proj_abc123
|
|
209
|
+
runhuman keys new "CI/CD Key" --project proj_abc123 # alias
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Options:**
|
|
213
|
+
- `--project <id>` - Project ID (required)
|
|
214
|
+
- `--copy` - Copy key to clipboard
|
|
215
|
+
|
|
216
|
+
#### `keys delete <keyId>`
|
|
217
|
+
|
|
218
|
+
Delete an API key permanently.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
runhuman keys delete key_abc123 --force
|
|
222
|
+
runhuman keys rm key_abc123 --force # alias
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Options:**
|
|
226
|
+
- `--force` - Skip confirmation prompt
|
|
227
|
+
|
|
228
|
+
### Templates
|
|
229
|
+
|
|
230
|
+
#### `templates list`
|
|
231
|
+
|
|
232
|
+
List all test templates for a project.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
runhuman templates list --project proj_abc123
|
|
236
|
+
runhuman templates ls --project proj_abc123 # alias
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Options:**
|
|
240
|
+
- `--project <id>` - Project ID (required)
|
|
241
|
+
|
|
242
|
+
#### `templates create <name>`
|
|
243
|
+
|
|
244
|
+
Create a new test template.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
runhuman templates create "Checkout Flow" --project proj_abc123
|
|
248
|
+
runhuman templates create "Login Test" --project proj_abc123 \
|
|
249
|
+
-d "Test login functionality" \
|
|
250
|
+
--duration 180 \
|
|
251
|
+
--screen-size mobile \
|
|
252
|
+
--schema ./schema.json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Options:**
|
|
256
|
+
- `--project <id>` - Project ID (required)
|
|
257
|
+
- `-d, --description <text>` - Template description
|
|
258
|
+
- `--duration <seconds>` - Target test duration
|
|
259
|
+
- `--screen-size <size>` - Default screen size
|
|
260
|
+
- `--schema <path>` - Path to JSON schema file
|
|
261
|
+
|
|
262
|
+
#### `templates show <templateId>`
|
|
263
|
+
|
|
264
|
+
Show details of a specific template.
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
runhuman templates show tmpl_abc123
|
|
268
|
+
runhuman templates info tmpl_abc123 # alias
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `templates update <templateId>`
|
|
272
|
+
|
|
273
|
+
Update a template.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
runhuman templates update tmpl_abc123 --name "New Name"
|
|
277
|
+
runhuman templates update tmpl_abc123 --description "New description"
|
|
278
|
+
runhuman templates update tmpl_abc123 --schema ./new-schema.json
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Options:**
|
|
282
|
+
- `--name <name>` - New template name
|
|
283
|
+
- `-d, --description <text>` - New description
|
|
284
|
+
- `--duration <seconds>` - New target duration
|
|
285
|
+
- `--screen-size <size>` - New default screen size
|
|
286
|
+
- `--schema <path>` - Path to new JSON schema file
|
|
287
|
+
|
|
288
|
+
#### `templates delete <templateId>`
|
|
289
|
+
|
|
290
|
+
Delete a template permanently.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
runhuman templates delete tmpl_abc123 --force
|
|
294
|
+
runhuman templates rm tmpl_abc123 --force # alias
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Options:**
|
|
298
|
+
- `--force` - Skip confirmation prompt
|
|
299
|
+
|
|
300
|
+
### GitHub Integration
|
|
301
|
+
|
|
302
|
+
#### `github link <repo>`
|
|
303
|
+
|
|
304
|
+
Link a GitHub repository to your project.
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
runhuman github link owner/repo --project proj_abc123
|
|
308
|
+
runhuman gh link volter-ai/runhuman --project proj_abc123 # alias
|
|
28
309
|
```
|
|
29
310
|
|
|
30
|
-
|
|
311
|
+
**Options:**
|
|
312
|
+
- `--project <id>` - Project ID (required)
|
|
31
313
|
|
|
32
|
-
|
|
314
|
+
#### `github repos`
|
|
315
|
+
|
|
316
|
+
List linked GitHub repositories.
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
runhuman github repos
|
|
320
|
+
runhuman github repos --project proj_abc123 # filter by project
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Options:**
|
|
324
|
+
- `--project <id>` - Filter by project
|
|
325
|
+
|
|
326
|
+
#### `github issues <repo>`
|
|
327
|
+
|
|
328
|
+
List GitHub issues for a repository.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
runhuman github issues owner/repo
|
|
332
|
+
runhuman github issues owner/repo --state open
|
|
333
|
+
runhuman github issues owner/repo --labels "bug,needs-qa"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Options:**
|
|
337
|
+
- `--state <state>` - Filter by state (open/closed/all, default: open)
|
|
338
|
+
- `--labels <labels>` - Filter by comma-separated labels
|
|
339
|
+
|
|
340
|
+
#### `github test <issueNumber>`
|
|
341
|
+
|
|
342
|
+
Create a QA test job for a GitHub issue.
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
runhuman github test 123 --repo owner/repo --url https://myapp.com
|
|
346
|
+
runhuman github test 123 --url https://myapp.com --sync # wait for result
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Options:**
|
|
350
|
+
- `--repo <owner/repo>` - Repository (or use default from config)
|
|
351
|
+
- `--url <url>` - URL to test (required)
|
|
352
|
+
- `--template <id>` - Template ID to use
|
|
353
|
+
- `--sync` - Wait for result before exiting
|
|
354
|
+
|
|
355
|
+
#### `github bulk-test`
|
|
356
|
+
|
|
357
|
+
Create QA test jobs for multiple GitHub issues.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Test all open issues
|
|
361
|
+
runhuman github bulk-test --repo owner/repo --url https://myapp.com
|
|
362
|
+
|
|
363
|
+
# Test issues with specific labels
|
|
364
|
+
runhuman github bulk-test --repo owner/repo --url https://myapp.com \
|
|
365
|
+
--labels "bug,needs-qa" \
|
|
366
|
+
--limit 5
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Options:**
|
|
370
|
+
- `--repo <owner/repo>` - Repository (or use default from config)
|
|
371
|
+
- `--url <url>` - URL to test (required)
|
|
372
|
+
- `--labels <labels>` - Filter issues by comma-separated labels
|
|
373
|
+
- `--state <state>` - Filter by state (open/closed/all, default: open)
|
|
374
|
+
- `--template <id>` - Template ID to use
|
|
375
|
+
- `--limit <number>` - Maximum number of jobs to create (default: 10)
|
|
376
|
+
|
|
377
|
+
### Authentication
|
|
378
|
+
|
|
379
|
+
#### `login`
|
|
380
|
+
|
|
381
|
+
Login to Runhuman and save your API key.
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
runhuman login
|
|
385
|
+
# Prompts for API key
|
|
386
|
+
|
|
387
|
+
# Or provide directly
|
|
388
|
+
runhuman login --api-key qa_live_xxxxxxxxxxxxx
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Get your API key at: [runhuman.com/dashboard](https://runhuman.com/dashboard)
|
|
392
|
+
|
|
393
|
+
#### `logout`
|
|
394
|
+
|
|
395
|
+
Logout and clear saved credentials.
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
runhuman logout
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
#### `whoami`
|
|
402
|
+
|
|
403
|
+
Display current user info and account balance.
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
runhuman whoami
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Configuration
|
|
410
|
+
|
|
411
|
+
#### `config get <key>`
|
|
412
|
+
|
|
413
|
+
Get a configuration value.
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
runhuman config get apiUrl
|
|
417
|
+
runhuman config get project
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### `config set <key> <value>`
|
|
421
|
+
|
|
422
|
+
Set a configuration value.
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# Set globally
|
|
426
|
+
runhuman config set apiUrl https://api.runhuman.com --global
|
|
427
|
+
|
|
428
|
+
# Set for current project
|
|
429
|
+
runhuman config set project proj_abc123
|
|
430
|
+
runhuman config set defaultUrl https://myapp.com
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Options:**
|
|
434
|
+
- `--global` - Save to global config instead of project config
|
|
435
|
+
|
|
436
|
+
#### `config list`
|
|
437
|
+
|
|
438
|
+
List all configuration values.
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
runhuman config list
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Shows configuration from all sources:
|
|
445
|
+
- Global config (`~/.config/runhuman/config.json`)
|
|
446
|
+
- Project config (`.runhumanrc`)
|
|
447
|
+
- Environment variables
|
|
448
|
+
- Effective (merged) config
|
|
449
|
+
|
|
450
|
+
#### `config reset`
|
|
451
|
+
|
|
452
|
+
Reset configuration to defaults.
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
# Reset project config
|
|
456
|
+
runhuman config reset project
|
|
457
|
+
|
|
458
|
+
# Reset global config
|
|
459
|
+
runhuman config reset global
|
|
460
|
+
|
|
461
|
+
# Reset all
|
|
462
|
+
runhuman config reset all
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### `init`
|
|
466
|
+
|
|
467
|
+
Initialize a new Runhuman project with interactive prompts.
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
runhuman init
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Creates a `.runhumanrc` file in the current directory with your project configuration.
|
|
474
|
+
|
|
475
|
+
## Configuration
|
|
476
|
+
|
|
477
|
+
### Configuration Hierarchy
|
|
478
|
+
|
|
479
|
+
Configuration is loaded from multiple sources with the following priority (highest to lowest):
|
|
480
|
+
|
|
481
|
+
1. **CLI flags** - `--api-key`, `--api-url`, etc.
|
|
482
|
+
2. **Environment variables** - `RUNHUMAN_API_KEY`, `RUNHUMAN_API_URL`, etc.
|
|
483
|
+
3. **Project config** - `.runhumanrc` in current directory
|
|
484
|
+
4. **Global config** - `~/.config/runhuman/config.json`
|
|
485
|
+
5. **Defaults**
|
|
486
|
+
|
|
487
|
+
### Environment Variables
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
RUNHUMAN_API_KEY # API key for authentication
|
|
491
|
+
RUNHUMAN_API_URL # API base URL (default: https://runhuman.com)
|
|
492
|
+
RUNHUMAN_PROJECT # Default project ID
|
|
493
|
+
RUNHUMAN_DEFAULT_URL # Default URL to test
|
|
494
|
+
RUNHUMAN_DEFAULT_DURATION # Default test duration in seconds
|
|
495
|
+
RUNHUMAN_DEFAULT_SCREEN_SIZE # Default screen size (desktop/mobile/tablet)
|
|
496
|
+
RUNHUMAN_OUTPUT_FORMAT # Output format (pretty/json/compact)
|
|
497
|
+
RUNHUMAN_NO_COLOR=1 # Disable colored output
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Project Config (`.runhumanrc`)
|
|
501
|
+
|
|
502
|
+
Create a `.runhumanrc` file in your project root:
|
|
33
503
|
|
|
34
504
|
```json
|
|
35
505
|
{
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
506
|
+
"project": "proj_abc123",
|
|
507
|
+
"defaultUrl": "https://myapp.com",
|
|
508
|
+
"defaultDuration": 300,
|
|
509
|
+
"defaultScreenSize": "desktop",
|
|
510
|
+
"githubRepo": "owner/repo",
|
|
511
|
+
"outputFormat": "pretty",
|
|
512
|
+
"color": true
|
|
42
513
|
}
|
|
43
514
|
```
|
|
44
515
|
|
|
45
|
-
|
|
46
|
-
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
47
|
-
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
516
|
+
### Global Config
|
|
48
517
|
|
|
49
|
-
|
|
518
|
+
Stored at `~/.config/runhuman/config.json`:
|
|
519
|
+
|
|
520
|
+
```json
|
|
521
|
+
{
|
|
522
|
+
"apiUrl": "https://runhuman.com",
|
|
523
|
+
"apiKey": "qa_live_xxxxxxxxxxxxx",
|
|
524
|
+
"color": true
|
|
525
|
+
}
|
|
526
|
+
```
|
|
50
527
|
|
|
51
|
-
##
|
|
528
|
+
## JSON Output
|
|
52
529
|
|
|
53
|
-
|
|
530
|
+
All commands support `--json` flag for machine-readable output:
|
|
54
531
|
|
|
55
532
|
```bash
|
|
56
|
-
|
|
57
|
-
runhuman
|
|
533
|
+
runhuman create https://myapp.com -d "Test" --json
|
|
534
|
+
runhuman status job_abc123 --json
|
|
535
|
+
runhuman list --json
|
|
536
|
+
```
|
|
58
537
|
|
|
59
|
-
|
|
60
|
-
runhuman list
|
|
538
|
+
JSON output format:
|
|
61
539
|
|
|
62
|
-
|
|
63
|
-
|
|
540
|
+
```json
|
|
541
|
+
{
|
|
542
|
+
"success": true,
|
|
543
|
+
"data": { ... },
|
|
544
|
+
"error": null
|
|
545
|
+
}
|
|
546
|
+
```
|
|
64
547
|
|
|
65
|
-
|
|
66
|
-
runhuman results <jobId>
|
|
548
|
+
## Exit Codes
|
|
67
549
|
|
|
68
|
-
|
|
550
|
+
- `0` - Success
|
|
551
|
+
- `1` - General error
|
|
552
|
+
- `2` - Authentication error
|
|
553
|
+
- `3` - Not found error
|
|
554
|
+
- `4` - Validation error
|
|
555
|
+
- `5` - Timeout error
|
|
556
|
+
|
|
557
|
+
## Examples
|
|
558
|
+
|
|
559
|
+
### Basic Workflow
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
# 1. Login
|
|
563
|
+
runhuman login
|
|
564
|
+
|
|
565
|
+
# 2. Initialize project
|
|
566
|
+
runhuman init
|
|
567
|
+
|
|
568
|
+
# 3. Create a test
|
|
569
|
+
runhuman create https://myapp.com -d "Test the login flow"
|
|
570
|
+
|
|
571
|
+
# 4. Wait for results
|
|
572
|
+
runhuman wait job_abc123
|
|
573
|
+
|
|
574
|
+
# 5. View detailed results
|
|
575
|
+
runhuman results job_abc123
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### CI/CD Integration
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
# Create test and wait for result (fail on error)
|
|
582
|
+
runhuman create https://staging.myapp.com \
|
|
583
|
+
-d "Smoke test on staging" \
|
|
584
|
+
--sync \
|
|
585
|
+
--json > result.json
|
|
586
|
+
|
|
587
|
+
# Exit code will be non-zero if test fails
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### GitHub Integration Workflow
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
# 1. Link your repo
|
|
594
|
+
runhuman github link owner/repo --project proj_abc123
|
|
595
|
+
|
|
596
|
+
# 2. List issues needing QA
|
|
597
|
+
runhuman github issues owner/repo --labels "needs-qa"
|
|
598
|
+
|
|
599
|
+
# 3. Test a specific issue
|
|
600
|
+
runhuman github test 42 --url https://myapp.com --sync
|
|
601
|
+
|
|
602
|
+
# 4. Bulk test multiple issues
|
|
603
|
+
runhuman github bulk-test --url https://myapp.com --labels "needs-qa" --limit 5
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Using Templates
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
# 1. Create a template
|
|
610
|
+
runhuman templates create "Login Flow Test" --project proj_abc123 \
|
|
611
|
+
-d "Test login with valid and invalid credentials" \
|
|
612
|
+
--duration 180 \
|
|
613
|
+
--schema ./schemas/login-test.json
|
|
614
|
+
|
|
615
|
+
# 2. Use the template
|
|
616
|
+
runhuman create https://myapp.com --template tmpl_abc123
|
|
617
|
+
|
|
618
|
+
# 3. List all templates
|
|
619
|
+
runhuman templates list --project proj_abc123
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## MCP Server for AI Agents
|
|
623
|
+
|
|
624
|
+
If you're using AI agents (Claude, GPT, etc.), check out our MCP server package:
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
npm install -g @runhuman/mcp
|
|
69
628
|
```
|
|
70
629
|
|
|
71
|
-
|
|
630
|
+
See [@runhuman/mcp](https://www.npmjs.com/package/@runhuman/mcp) for details.
|
|
72
631
|
|
|
73
632
|
## Learn More
|
|
74
633
|
|
|
75
634
|
- **Website:** [runhuman.com](https://runhuman.com)
|
|
76
635
|
- **Documentation:** [runhuman.com/docs](https://runhuman.com/docs)
|
|
77
|
-
- **MCP Server:** [@runhuman/mcp](https://www.npmjs.com/package/@runhuman/mcp)
|
|
78
636
|
- **GitHub:** [github.com/volter-ai/runhuman](https://github.com/volter-ai/runhuman)
|
|
79
637
|
|
|
80
638
|
## Support
|
|
@@ -84,11 +642,11 @@ runhuman results <jobId>
|
|
|
84
642
|
|
|
85
643
|
## How It Works
|
|
86
644
|
|
|
87
|
-
1. **
|
|
645
|
+
1. **You create a job** - Define test instructions and optional output schema
|
|
88
646
|
2. **Job posted to testers** - Request goes to human tester pool
|
|
89
647
|
3. **Human performs test** - Real person tests with video/screenshot recording
|
|
90
648
|
4. **AI extracts data** - GPT-4o processes feedback into structured JSON
|
|
91
|
-
5. **
|
|
649
|
+
5. **You get results** - Clean, typed data ready for automation
|
|
92
650
|
|
|
93
651
|
**Pricing:** Pay-per-second ($0.0018/sec of tester time). No monthly fees, no minimums.
|
|
94
652
|
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var Oe=Object.defineProperty;var De=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(t,e)=>(typeof require<"u"?require:t)[e]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var H=(r,t)=>()=>(r&&(t=r(r=0)),t);var Ee=(r,t)=>{for(var e in t)Oe(r,e,{get:t[e],enumerable:!0})};function Ne(r){return r instanceof S}function p(r){return Ne(r)?{message:r.message,exitCode:r.exitCode,details:r.details}:r instanceof Error?{message:r.message,exitCode:1}:{message:String(r),exitCode:1}}var S,q,_,N,z,w=H(()=>{"use strict";S=class extends Error{constructor(e,n=1,o){super(e);this.exitCode=n;this.details=o;this.name="CliError"}},q=class extends S{constructor(t="Authentication failed",e){super(t,2,e),this.name="AuthenticationError"}},_=class extends S{constructor(t="Resource not found",e){super(t,3,e),this.name="NotFoundError"}},N=class extends S{constructor(t="Validation failed",e){super(t,4,e),this.name="ValidationError"}},z=class extends S{constructor(t="Operation timed out",e){super(t,5,e),this.name="TimeoutError"}}});import ve from"axios";var d,I=H(()=>{"use strict";w();d=class{client;config;constructor(t){this.config=t,this.client=ve.create({baseURL:t.apiUrl||"https://runhuman.com",timeout:3e4,headers:{"Content-Type":"application/json"}}),this.client.interceptors.request.use(e=>(this.config.apiKey&&(e.headers.Authorization=`Bearer ${this.config.apiKey}`),e)),this.client.interceptors.response.use(e=>e,e=>{throw this.handleError(e)})}handleError(t){if(t.response){let e=t.response.status,n=t.response.data,o=n?.error||n?.message||t.message;switch(e){case 401:case 403:return new q(o,n);case 404:return new _(o,n);case 400:case 422:return new N(o,n);default:return new S(o,1,n)}}return t.code==="ECONNABORTED"?new S("Request timeout",5):t.code==="ENOTFOUND"||t.code==="ECONNREFUSED"?new S("Cannot connect to Runhuman API",1):new S(t.message,1)}async createJob(t){return(await this.client.post("/jobs",t)).data}async getJob(t){return(await this.client.get(`/job/${t}`)).data}async listJobs(t){return(await this.client.get("/jobs",{params:t})).data}async cancelJob(t){await this.client.post(`/job/${t}/cancel`)}async deleteJob(t){await this.client.delete(`/job/${t}`)}async listProjects(t){return(await this.client.get("/projects",{params:t})).data}async getProject(t){return(await this.client.get(`/projects/${t}`)).data}async createProject(t){return(await this.client.post("/projects",t)).data}async updateProject(t,e){return(await this.client.put(`/projects/${t}`,e)).data}async deleteProject(t){await this.client.delete(`/projects/${t}`)}async listApiKeys(t){return(await this.client.get(`/projects/${t}/keys`)).data}async createApiKey(t,e){return(await this.client.post(`/projects/${t}/keys`,{name:e})).data}async deleteApiKey(t){await this.client.delete(`/keys/${t}`)}async listTemplates(t){return(await this.client.get(`/projects/${t}/templates`)).data}async getTemplate(t){return(await this.client.get(`/templates/${t}`)).data}async createTemplate(t,e){return(await this.client.post(`/projects/${t}/templates`,e)).data}async updateTemplate(t,e){return(await this.client.put(`/templates/${t}`,e)).data}async deleteTemplate(t){await this.client.delete(`/templates/${t}`)}async getCurrentUser(){return(await this.client.get("/auth/me")).data}async getTokenBalance(){return(await this.client.get("/auth/balance")).data}async linkGithubRepo(t,e,n){return(await this.client.post(`/projects/${t}/github/link`,{owner:e,repo:n})).data}async listGithubRepos(t){let e=t?{projectId:t}:void 0;return(await this.client.get("/github/repos",{params:e})).data}async listGithubIssues(t,e,n){return(await this.client.get(`/github/${t}/${e}/issues`,{params:n})).data}async getGithubIssue(t,e,n){return(await this.client.get(`/github/${t}/${e}/issues/${n}`)).data}}});import{cosmiconfig as Te}from"cosmiconfig";import{homedir as Me}from"os";import{join as O}from"path";import{readFileSync as $,writeFileSync as x,existsSync as A,mkdirSync as Y,chmodSync as Je}from"fs";function Ke(r){return Le.includes(r)}var $e,R,v,L,Le,m,b=H(()=>{"use strict";$e="runhuman",R=O(Me(),".config","runhuman"),v=O(R,"config.json"),L=O(R,"credentials.json"),Le=["pretty","json","compact"];m=class{constructor(t=process.cwd()){this.cwd=t}projectConfig=null;globalConfig=null;envConfig=null;async loadConfig(t={}){return this.envConfig=this.loadEnvConfig(),this.projectConfig=await this.loadProjectConfig(),this.globalConfig=this.loadGlobalConfig(),{...this.getDefaults(),...this.globalConfig,...this.projectConfig,...this.envConfig,...t}}getDefaults(){return{apiUrl:"https://runhuman.com",outputFormat:"pretty",color:!0,autoOpenBrowser:!0,defaultDuration:5,defaultScreenSize:"desktop"}}loadEnvConfig(){let t={};return process.env.RUNHUMAN_API_KEY&&(t.apiKey=process.env.RUNHUMAN_API_KEY),process.env.RUNHUMAN_API_URL&&(t.apiUrl=process.env.RUNHUMAN_API_URL),process.env.RUNHUMAN_PROJECT&&(t.project=process.env.RUNHUMAN_PROJECT),process.env.RUNHUMAN_DEFAULT_URL&&(t.defaultUrl=process.env.RUNHUMAN_DEFAULT_URL),process.env.RUNHUMAN_DEFAULT_DURATION&&(t.defaultDuration=parseInt(process.env.RUNHUMAN_DEFAULT_DURATION,10)),process.env.RUNHUMAN_DEFAULT_SCREEN_SIZE&&(t.defaultScreenSize=process.env.RUNHUMAN_DEFAULT_SCREEN_SIZE),process.env.RUNHUMAN_OUTPUT_FORMAT&&Ke(process.env.RUNHUMAN_OUTPUT_FORMAT)&&(t.outputFormat=process.env.RUNHUMAN_OUTPUT_FORMAT),process.env.RUNHUMAN_NO_COLOR==="1"&&(t.color=!1),t}async loadProjectConfig(){try{return(await Te($e).search(this.cwd))?.config||null}catch{return null}}loadGlobalConfig(){try{if(!A(v))return null;let t=$(v,"utf-8");return JSON.parse(t)}catch{return null}}async get(t){return(await this.loadConfig())[t]}async set(t,e,n=!1){n?await this.setGlobalConfig(t,e):await this.setProjectConfig(t,e)}async setGlobalConfig(t,e){A(R)||Y(R,{recursive:!0});let n={};if(A(v)){let o=$(v,"utf-8");n=JSON.parse(o)}n[t]=e,x(v,JSON.stringify(n,null,2))}async setProjectConfig(t,e){let n=O(this.cwd,".runhumanrc"),o={};if(A(n)){let s=$(n,"utf-8");o=JSON.parse(s)}o[t]=e,x(n,JSON.stringify(o,null,2))}async saveProjectConfig(t){let e=O(this.cwd,".runhumanrc"),n={};if(A(e)){let s=$(e,"utf-8");n=JSON.parse(s)}let o={...n,...t};x(e,JSON.stringify(o,null,2))}async list(){let t=await this.loadConfig();return{global:this.globalConfig,project:this.projectConfig,env:this.envConfig,effective:t}}async reset(t){if((t==="global"||t==="all")&&A(v)&&x(v,"{}"),t==="project"||t==="all"){let e=O(this.cwd,".runhumanrc");A(e)&&x(e,"{}")}}saveCredentials(t){A(R)||Y(R,{recursive:!0}),x(L,JSON.stringify(t,null,2));try{process.platform!=="win32"&&Je(L,384)}catch{}}loadCredentials(){try{if(!A(L))return null;let t=$(L,"utf-8");return JSON.parse(t)}catch{return null}}clearCredentials(){A(L)&&x(L,"{}")}saveUserInfo(t){let e=O(R,"user.json");A(R)||Y(R,{recursive:!0}),x(e,JSON.stringify(t,null,2))}loadUserInfo(){try{let t=O(R,"user.json");if(!A(t))return null;let e=$(t,"utf-8");return JSON.parse(e)}catch{return null}}clearUserInfo(){let t=O(R,"user.json");A(t)&&x(t,"{}")}}});import T from"chalk";import Fe from"cli-table3";var a,j=H(()=>{"use strict";a=class{constructor(t={}){this.options=t}output(t){this.options.json?console.log(JSON.stringify(t,null,2)):t.success?t.data&&!this.options.quiet&&this.outputPretty(t.data):this.outputError(t.error?.message||"An error occurred")}outputPretty(t){typeof t=="string"?console.log(t):(Array.isArray(t),console.log(JSON.stringify(t,null,2)))}outputError(t,e){if(this.options.json){let n={success:!1,error:{message:t,details:e},timestamp:new Date().toISOString()};console.error(JSON.stringify(n,null,2))}else console.error(this.color("red",`
|
|
3
|
+
Error: ${t}
|
|
4
|
+
`)),e&&console.error(this.color("gray",JSON.stringify(e,null,2)))}success(t){!this.options.json&&!this.options.quiet&&console.log(this.color("green",`
|
|
5
|
+
${t}
|
|
6
|
+
`))}info(t){!this.options.json&&!this.options.quiet&&console.log(this.color("blue",t))}warn(t){!this.options.json&&!this.options.quiet&&console.warn(this.color("yellow",`Warning: ${t}`))}formatJobList(t){if(this.options.format==="compact")return t.map(n=>`${n.id} ${n.status} ${n.url}`).join(`
|
|
7
|
+
`);let e=new Fe({head:["Job ID","Status","URL","Created","Duration","Cost"].map(n=>this.color("cyan",n)),colWidths:[15,12,30,14,10,10]});return t.forEach(n=>{e.push([n.id,this.formatStatus(n.status),this.truncate(n.url,28),this.formatDate(n.createdAt),n.testDurationSeconds?this.formatDuration(n.testDurationSeconds):"-",n.costUsd?`$${n.costUsd.toFixed(3)}`:"-"])}),e.toString()}formatStatus(t){let e={pending:"yellow",waiting:"blue",working:"blue",creating_issues:"cyan",completed:"green",incomplete:"yellow",abandoned:"red",rejected:"gray",error:"red",failed:"red"},n=t.replace("_"," ");return this.color(e[t]||"white",n)}formatDuration(t){if(t<60)return`${t}s`;let e=Math.floor(t/60),n=t%60;if(e<60)return`${e}m ${n}s`;let o=Math.floor(e/60),s=e%60;return`${o}h ${s}m ${n}s`}formatDate(t){let e=new Date(t),o=new Date().getTime()-e.getTime(),s=Math.floor(o/1e3),u=Math.floor(s/60),i=Math.floor(u/60),c=Math.floor(i/24);return s<60?`${s}s ago`:u<60?`${u}m ago`:i<24?`${i}h ago`:c<30?`${c}d ago`:e.toLocaleDateString()}truncate(t,e){return t.length<=e?t:t.substring(0,e-3)+"..."}color(t,e){if(this.options.color===!1)return e;let o={red:T.red,green:T.green,blue:T.blue,yellow:T.yellow,cyan:T.cyan,gray:T.gray,white:T.white}[t];return o?o(e):e}static result(t){return{success:!0,data:t,timestamp:new Date().toISOString()}}static error(t,e,n){return{success:!1,error:{message:t,code:e,details:n},timestamp:new Date().toISOString()}}}});var X={};Ee(X,{waitCommand:()=>Z,waitForJob:()=>oe});import{Command as He}from"commander";import Ge from"ora";async function oe(r,t,e,n=600){let o=Date.now(),s=n*1e3,u=1e4,i=null;for(e.options.json||(i=Ge("\u23F3 Waiting for job completion...").start());;){let c=Date.now()-o;if(c>=s)throw i&&i.fail("Timeout waiting for job completion"),new z(`Job did not complete within ${n} seconds`);let l=await t.getJob(r);if(i){let g=e.formatDuration(Math.floor(c/1e3));i.text=`\u23F3 Waiting for job completion... (${g} elapsed, status: ${l.status})`}if(l.status==="completed"){if(i&&i.succeed("\u2705 Test Completed!"),!e.options.json){if(console.log(`
|
|
8
|
+
Duration: `+(l.testDurationSeconds?e.formatDuration(l.testDurationSeconds):"N/A")),console.log(" Cost: $"+(l.costUsd||0).toFixed(3)),l.testerName&&console.log(" Tester: "+l.testerName),l.result){console.log(`
|
|
9
|
+
\u{1F4CB} Results Summary:`);for(let[g,f]of Object.entries(l.result))console.log(` ${g}: ${f}`)}console.log(`
|
|
10
|
+
\u{1F4BE} Full results:`),console.log(` runhuman results ${r}
|
|
11
|
+
`)}return}if(l.status==="error"||l.status==="incomplete"||l.status==="abandoned")throw i&&i.fail(`Test ${l.status}`),new Error(`Job ${l.status}: ${l.testerResponse||"No details available"}`);if(l.status==="rejected")throw i&&i.fail("Test was rejected"),new Error("Job was rejected");await new Promise(g=>setTimeout(g,u))}}function Z(){let r=new He("wait");return r.description("Wait for a job to complete and display results").argument("<jobId>","Job ID to wait for").option("--timeout <seconds>","Max wait time (default: 600)",parseInt).option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),u=new d(o);if(await oe(t,u,s,e.timeout||600),e.json){let i=await u.getJob(t),c=a.result(i);s.output(c)}}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}var W=H(()=>{"use strict";I();b();j();w()});import{Command as vt}from"commander";I();b();j();w();import{Command as qe}from"commander";import _e from"ora";function ne(){let r=new qe("create");return r.description("Create a new QA test job").argument("[url]","URL to test").option("-d, --description <text>","Test instructions for the human tester").option("-t, --template <name>","Use a template as base configuration").option("--duration <minutes>","Target duration (1-60 minutes)",parseInt).option("--screen-size <preset>","Screen size: desktop|laptop|tablet|mobile").option("--schema <file>","Path to JSON schema file for structured output").option("--schema-inline <json>","Inline JSON schema").option("--metadata <json>","Metadata for tracking (JSON string)").option("--github-repo <owner/repo>","GitHub repo for context").option("--create-issues","Auto-create GitHub issues from findings").option("--sync","Wait for result before exiting (synchronous mode)").option("--wait <seconds>","Max wait time in sync mode (default: 300)",parseInt).option("--project <id>","Project ID (or use default from config)").option("--api-key <key>","API key (or use from config/env)").option("--json","Output as JSON (for scripting)").option("--quiet","Minimal output (only job ID)").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,project:e.project}),s=new a({json:e.json,quiet:e.quiet,color:o.color}),u=new d(o);if(!t&&!e.template&&!o.defaultUrl)throw new N("URL is required (provide as argument, via --template, or set defaultUrl in config)");if(!e.description&&!e.template)throw new N("Description is required (use -d flag or --template)");let i={url:t||o.defaultUrl||"",description:e.description||"",duration:e.duration||o.defaultDuration,screenSize:e.screenSize||o.defaultScreenSize};if(e.schema){let f=await(await import("fs/promises")).readFile(e.schema,"utf-8");i.schema=JSON.parse(f)}else e.schemaInline&&(i.schema=JSON.parse(e.schemaInline));e.metadata&&(i.metadata=JSON.parse(e.metadata)),e.githubRepo&&(i.githubRepo=e.githubRepo),e.createIssues&&(i.createIssues=!0),e.template&&(i.templateId=e.template);let c=e.json?null:_e("Creating QA test job...").start(),l=await u.createJob(i);if(c&&c.succeed("Job created successfully!"),e.json){let g=a.result({jobId:l.jobId,status:l.status,message:l.message,dashboardUrl:`${o.apiUrl}/jobs/${l.jobId}`,estimatedCompletionTime:l.estimatedCompletionTime});s.output(g)}else e.quiet?console.log(l.jobId):(console.log(`
|
|
12
|
+
`+"=".repeat(60)),console.log(" Job ID: "+l.jobId),console.log(" Status: "+l.status),console.log(` Dashboard: ${o.apiUrl}/jobs/${l.jobId}`),console.log("=".repeat(60)+`
|
|
13
|
+
`),console.log("\u{1F4A1} Track progress:"),console.log(` runhuman status ${l.jobId}`),console.log(` runhuman wait ${l.jobId}
|
|
14
|
+
`));if(e.sync){let{waitForJob:g}=await Promise.resolve().then(()=>(W(),X));await g(l.jobId,u,s,e.wait||300)}}catch(n){let o=p(n);new a({json:e.json,quiet:!1}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}I();b();j();w();import{Command as ze}from"commander";function re(){let r=new ze("status");return r.description("Check the current status of a job").argument("<jobId>","Job ID to check").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),i=await new d(o).getJob(t);if(e.json){let c=a.result(i);s.output(c)}else console.log(`
|
|
15
|
+
\u{1F4CA} Job Status: `+t+`
|
|
16
|
+
`),console.log(" Status: "+s.formatStatus(i.status)),i.testerName&&console.log(" Tester: "+i.testerName),console.log(" URL: "+i.url),console.log(" Description: "+i.description),console.log(" Created: "+i.createdAt),i.completedAt&&console.log(" Completed: "+i.completedAt),i.testDurationSeconds&&console.log(" Duration: "+s.formatDuration(i.testDurationSeconds)),i.costUsd&&console.log(" Cost: $"+i.costUsd.toFixed(3)),console.log(`
|
|
17
|
+
Dashboard: ${o.apiUrl}/jobs/${t}
|
|
18
|
+
`),i.status==="pending"||i.status==="waiting"||i.status==="working"?(console.log("\u{1F4A1} Wait for completion:"),console.log(` runhuman wait ${t}
|
|
19
|
+
`)):i.status==="completed"&&(console.log("\u{1F4A1} View results:"),console.log(` runhuman results ${t}
|
|
20
|
+
`))}catch(n){let o=new a({json:e.json}),s=p(n);o.outputError(s.message,s.details),process.exit(s.exitCode)}}),r}W();I();b();j();w();import{Command as We}from"commander";function se(){let r=new We("results");return r.description("Display detailed results for a completed job").argument("<jobId>","Job ID to show results for").option("--json","Output as JSON").option("--schema-only","Show only extracted schema data").option("--raw","Show raw tester response (no processing)").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),i=await new d(o).getJob(t);if(e.json){let c=a.result(i);s.output(c)}else{if(console.log(`
|
|
21
|
+
\u{1F4CA} Test Results: `+t+`
|
|
22
|
+
`),console.log("=".repeat(80)),console.log("Job Information"),console.log("=".repeat(80)+`
|
|
23
|
+
`),console.log(" Job ID: "+i.id),console.log(" Status: "+i.status),console.log(" URL: "+i.url),console.log(" Description: "+i.description),console.log(" Started: "+i.createdAt),i.completedAt&&console.log(" Completed: "+i.completedAt),i.testDurationSeconds&&console.log(" Duration: "+s.formatDuration(i.testDurationSeconds)),i.costUsd&&console.log(" Cost: $"+i.costUsd.toFixed(3)),i.testerName&&console.log(" Tester: "+i.testerName),i.result&&Object.keys(i.result).length>0&&!e.raw){console.log(`
|
|
24
|
+
`+"=".repeat(80)),console.log("Structured Results (Extracted)"),console.log("=".repeat(80)+`
|
|
25
|
+
`);for(let[c,l]of Object.entries(i.result)){let g=typeof l=="object"?JSON.stringify(l,null,2):String(l);console.log(` ${c}:`.padEnd(30)+g)}}i.testerResponse&&!e.schemaOnly&&(console.log(`
|
|
26
|
+
`+"=".repeat(80)),console.log("Tester Feedback"),console.log("=".repeat(80)+`
|
|
27
|
+
`),console.log(" "+i.testerResponse.split(`
|
|
28
|
+
`).join(`
|
|
29
|
+
`))),console.log(`
|
|
30
|
+
`+"=".repeat(80)+`
|
|
31
|
+
`)}}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}I();b();j();w();import{Command as Be}from"commander";function ie(){let r=new Be("list");return r.description("List all jobs with optional filtering").argument("[filter]","Status filter: all|pending|claimed|in_progress|completed|failed|timeout").option("--project <id>","Filter by project").option("--limit <number>","Number of results (default: 20)",parseInt).option("--offset <number>","Pagination offset (default: 0)",parseInt).option("--json","Output as JSON").option("--format <type>","Output format: table|json|compact").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,format:e.format,color:o.color}),u=new d(o),i={limit:e.limit||20,offset:e.offset||0};t&&t!=="all"&&(i.status=t),e.project&&(i.projectId=e.project);let{jobs:c,total:l}=await u.listJobs(i);if(e.json){let g=a.result({jobs:c,total:l});s.output(g)}else console.log(`
|
|
32
|
+
\u{1F4CB} Recent Jobs (${c.length} of ${l})
|
|
33
|
+
`),console.log(s.formatJobList(c)),console.log(`
|
|
34
|
+
\u{1F4A1} View details: runhuman status <jobId>`),console.log(` View results: runhuman results <jobId>
|
|
35
|
+
`)}catch(n){let o=new a({json:e.json}),s=p(n);o.outputError(s.message,s.details),process.exit(s.exitCode)}}),r}I();b();j();w();import{Command as Ve}from"commander";function ae(){let r=new Ve("delete");return r.description("Delete a job permanently").argument("<jobId>","Job ID to delete").option("--confirm","Skip confirmation prompt").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color});if(!e.json&&!e.confirm)throw console.log(`
|
|
36
|
+
\u26A0\uFE0F Warning: This will permanently delete job ${t}`),console.log(` This action cannot be undone.
|
|
37
|
+
`),new Error("Please use --confirm flag to delete the job");if(await new d(o).deleteJob(t),e.json){let i=a.result({success:!0,message:"Job deleted successfully",jobId:t});s.output(i)}else console.log(`
|
|
38
|
+
\u2705 Job deleted successfully
|
|
39
|
+
`),console.log(` Job ID: ${t}`),console.log(` Status: Permanently deleted
|
|
40
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}I();b();j();w();import{Command as Qe}from"commander";import Ye from"chokidar";import{existsSync as B,readFileSync as ee,writeFileSync as Ze,unlinkSync as Xe}from"fs";import{join as ce}from"path";import{homedir as le}from"os";var E=ce(le(),".config","runhuman","watch.pid");function ue(){let r=new Qe("watch");return r.description("Watch files and auto-create QA test jobs on changes").argument("[patterns...]",'File patterns to watch (e.g., "src/**/*.tsx")').option("--url <url>","URL to test (required)").option("--template <name>","Template to use for tests").option("--description <text>","Test description").option("--debounce <ms>","Debounce delay in milliseconds",parseInt).option("--ignore <patterns...>","Patterns to ignore").option("--stop","Stop existing watch process").option("--status","Check if watch is running").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{if(e.stop){await tt();return}if(e.status){ot();return}pe()&&(console.log(`
|
|
41
|
+
\u26A0\uFE0F Watch mode is already running`),console.log(` Use --stop to stop it first
|
|
42
|
+
`),process.exit(1));let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=o.watch||{},u=t.length>0?t:s.patterns||["src/**/*"],i=e.ignore||s.ignore||["**/node_modules/**","**/dist/**","**/build/**","**/.git/**"],c=e.debounce||s.debounce||2e3,l=e.url||s.url||o.defaultUrl,g=e.description||s.description||"Auto-test from watch mode",f=e.template||s.template;l||(console.log(`
|
|
43
|
+
\u274C Error: URL required
|
|
44
|
+
`),console.log(`Provide via --url flag or set in .runhumanrc:
|
|
45
|
+
`),console.log(' runhuman watch --url "https://myapp.com"'),console.log(` OR add to .runhumanrc: { "watch": { "url": "..." } }
|
|
46
|
+
`),process.exit(1));let h=new a({color:o.color});console.log(`
|
|
47
|
+
\u{1F440} Starting watch mode
|
|
48
|
+
`),console.log(` Watching: ${u.join(", ")}`),console.log(` Ignoring: ${i.join(", ")}`),console.log(` Debounce: ${c}ms`),console.log(` URL: ${l}`),f&&console.log(` Template: ${f}`),console.log(`
|
|
49
|
+
Press Ctrl+C to stop
|
|
50
|
+
`),et(process.pid);let U=new d(o),y=null,D=new Set,P=Ye.watch(u,{ignored:i,persistent:!0,ignoreInitial:!0});P.on("change",J=>{D.add(J),console.log(` \u{1F4DD} Changed: ${J}`),y&&clearTimeout(y),y=setTimeout(async()=>{let te=Array.from(D);D.clear(),console.log(`
|
|
51
|
+
\u{1F680} Creating test job for ${te.length} file(s)...
|
|
52
|
+
`);try{let Q={url:l,description:`${g}
|
|
53
|
+
|
|
54
|
+
Changed files:
|
|
55
|
+
${te.map(xe=>`- ${xe}`).join(`
|
|
56
|
+
`)}`,templateId:f},F=await U.createJob(Q);console.log(` \u2705 Job created: ${F.jobId}`),console.log(` \u{1F4CA} Dashboard: https://runhuman.com/jobs/${F.jobId}
|
|
57
|
+
`),console.log(` Watching for more changes...
|
|
58
|
+
`)}catch(Q){let F=p(Q);h.outputError(F.message,F.details),console.log(`
|
|
59
|
+
Watching for more changes...
|
|
60
|
+
`)}},c)}),P.on("error",J=>{console.error(`
|
|
61
|
+
\u274C Watch error: ${J instanceof Error?J.message:String(J)}
|
|
62
|
+
`)});let K=()=>{console.log(`
|
|
63
|
+
|
|
64
|
+
\u{1F44B} Stopping watch mode...
|
|
65
|
+
`),P.close(),G(),process.exit(0)};process.on("SIGINT",K),process.on("SIGTERM",K)}catch(n){let o=p(n);new a({}).outputError(o.message,o.details),G(),process.exit(o.exitCode)}}),r}function et(r){let t=ce(le(),".config","runhuman");B(t)||De("fs").mkdirSync(t,{recursive:!0}),Ze(E,r.toString())}function G(){B(E)&&Xe(E)}function pe(){if(!B(E))return!1;try{let r=parseInt(ee(E,"utf-8"));return process.kill(r,0),!0}catch{return G(),!1}}async function tt(){if(!B(E)){console.log(`
|
|
66
|
+
\u2139\uFE0F Watch mode is not running
|
|
67
|
+
`);return}try{let r=parseInt(ee(E,"utf-8"));process.kill(r,"SIGTERM"),G(),console.log(`
|
|
68
|
+
\u2705 Watch mode stopped
|
|
69
|
+
`)}catch{G(),console.log(`
|
|
70
|
+
\u2139\uFE0F Watch process not found (already stopped)
|
|
71
|
+
`)}}function ot(){if(pe()){let r=parseInt(ee(E,"utf-8"));console.log(`
|
|
72
|
+
\u2705 Watch mode is running`),console.log(` \u{1F4DD} PID: ${r}`),console.log(`
|
|
73
|
+
Stop with: runhuman watch --stop
|
|
74
|
+
`)}else console.log(`
|
|
75
|
+
\u2139\uFE0F Watch mode is not running
|
|
76
|
+
`)}I();b();w();j();import{Command as at}from"commander";import nt from"http";import{URL as rt}from"url";import st from"open";async function V(r){let{apiUrl:t,autoOpenBrowser:e=!0}=r;return new Promise((n,o)=>{let s=nt.createServer((i,c)=>{if(!i.url){c.writeHead(400),c.end("Bad request");return}let l=new rt(i.url,"http://localhost");if(l.pathname==="/callback"){let g=l.searchParams.get("token"),f=l.searchParams.get("email"),h=l.searchParams.get("error");if(h){c.writeHead(200,{"Content-Type":"text/html"}),c.end(me(h)),s.close(),o(new Error(h));return}if(g&&f){c.writeHead(200,{"Content-Type":"text/html"}),c.end(it(f)),s.close(),n({token:g,email:f});return}c.writeHead(400,{"Content-Type":"text/html"}),c.end(me("Missing token or email in callback")),s.close(),o(new Error("Missing token or email in callback"));return}c.writeHead(404),c.end("Not found")});s.listen(0,"127.0.0.1",async()=>{let i=s.address();if(!i||typeof i=="string"){o(new Error("Failed to start local server"));return}let l=`http://127.0.0.1:${i.port}/callback`,g=`${t}/cli/auth?callback=${encodeURIComponent(l)}`;if(console.log(""),e){console.log("Opening browser for authentication...");try{await st(g)}catch{console.log("Could not open browser automatically."),console.log(`
|
|
77
|
+
Please visit: ${g}`)}}else console.log(`Please visit: ${g}`);console.log(""),console.log("Waiting for authentication...")});let u=setTimeout(()=>{s.close(),o(new Error("Authentication timed out. Please try again."))},300*1e3);s.on("close",()=>clearTimeout(u)),s.on("error",i=>{clearTimeout(u),o(new Error(`Failed to start local server: ${i.message}`))})})}function it(r){return`<!DOCTYPE html>
|
|
78
|
+
<html>
|
|
79
|
+
<head>
|
|
80
|
+
<title>Runhuman CLI - Authenticated</title>
|
|
81
|
+
<style>
|
|
82
|
+
body {
|
|
83
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
84
|
+
display: flex;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
align-items: center;
|
|
87
|
+
min-height: 100vh;
|
|
88
|
+
margin: 0;
|
|
89
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
90
|
+
color: white;
|
|
91
|
+
}
|
|
92
|
+
.container {
|
|
93
|
+
text-align: center;
|
|
94
|
+
padding: 40px;
|
|
95
|
+
background: rgba(255,255,255,0.1);
|
|
96
|
+
border-radius: 16px;
|
|
97
|
+
backdrop-filter: blur(10px);
|
|
98
|
+
}
|
|
99
|
+
h1 { margin-bottom: 16px; }
|
|
100
|
+
p { opacity: 0.9; }
|
|
101
|
+
.email { font-weight: bold; }
|
|
102
|
+
.close-note { margin-top: 24px; font-size: 14px; opacity: 0.7; }
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<div class="container">
|
|
107
|
+
<h1>Successfully authenticated!</h1>
|
|
108
|
+
<p>Logged in as <span class="email">${r}</span></p>
|
|
109
|
+
<p class="close-note">You can close this tab and return to the terminal.</p>
|
|
110
|
+
</div>
|
|
111
|
+
</body>
|
|
112
|
+
</html>`}function me(r){return`<!DOCTYPE html>
|
|
113
|
+
<html>
|
|
114
|
+
<head>
|
|
115
|
+
<title>Runhuman CLI - Authentication Failed</title>
|
|
116
|
+
<style>
|
|
117
|
+
body {
|
|
118
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
119
|
+
display: flex;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
align-items: center;
|
|
122
|
+
min-height: 100vh;
|
|
123
|
+
margin: 0;
|
|
124
|
+
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
125
|
+
color: white;
|
|
126
|
+
}
|
|
127
|
+
.container {
|
|
128
|
+
text-align: center;
|
|
129
|
+
padding: 40px;
|
|
130
|
+
background: rgba(255,255,255,0.1);
|
|
131
|
+
border-radius: 16px;
|
|
132
|
+
backdrop-filter: blur(10px);
|
|
133
|
+
}
|
|
134
|
+
h1 { margin-bottom: 16px; }
|
|
135
|
+
.error { background: rgba(0,0,0,0.2); padding: 12px; border-radius: 8px; margin-top: 16px; }
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
<div class="container">
|
|
140
|
+
<h1>Authentication failed</h1>
|
|
141
|
+
<p>Please try again from the terminal.</p>
|
|
142
|
+
<div class="error">${r}</div>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`}function ge(){let r=new at("login");return r.description("Authenticate with Runhuman").option("--token <token>","Login with API key (skip browser)").option("--no-browser","Print auth URL instead of opening browser").option("--json","Output as JSON").action(async t=>{try{let e=new m,n=await e.loadConfig(),o=new a({json:t.json,color:n.color});if(t.token)await ct(e,o,t.token,t.json);else{let s=t.browser!==!1&&n.autoOpenBrowser!==!1;await lt(e,o,n.apiUrl,s,t.json)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}async function ct(r,t,e,n){r.saveCredentials({accessToken:e});let o=await r.loadConfig({apiKey:e}),u=await new d(o).getCurrentUser();r.saveUserInfo(u),n?t.output(a.result({success:!0,user:u})):(t.success("Successfully logged in!"),console.log(` User: ${u.email}`),console.log(` Account ID: ${u.accountId}
|
|
146
|
+
`))}async function lt(r,t,e,n,o){o||console.log("Logging in to Runhuman...");let s=await V({apiUrl:e||"https://runhuman.com",autoOpenBrowser:n});r.saveCredentials({accessToken:s.token});let u=await r.loadConfig({apiKey:s.token}),c=await new d(u).getCurrentUser();r.saveUserInfo(c),o?t.output(a.result({success:!0,user:c})):(console.log(""),t.success("Successfully logged in!"),console.log(` User: ${c.email}`),console.log(` Account ID: ${c.accountId}
|
|
147
|
+
`))}b();w();j();import{Command as ut}from"commander";function de(){let r=new ut("logout");return r.description("Log out and clear stored credentials").option("--force","Skip confirmation prompt").option("--json","Output as JSON").action(async t=>{try{let e=new m,n=new a({json:t.json});if(e.clearCredentials(),e.clearUserInfo(),t.json){let o=a.result({success:!0,message:"Logged out successfully"});n.output(o)}else n.success("Logged out successfully!"),console.log(`Credentials have been cleared.
|
|
148
|
+
`)}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}I();b();w();j();import{Command as pt}from"commander";function fe(){let r=new pt("whoami");return r.description("Display current user information").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let e=new m,n=await e.loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=new a({json:t.json}),s=new d(n),u=await s.getCurrentUser(),i;try{i=(await s.getTokenBalance()).balance}catch{i=null}let c=e.loadUserInfo();if(t.json){let l=a.result({user:u,balance:i});o.output(l)}else console.log(`
|
|
149
|
+
\u{1F464} Current User
|
|
150
|
+
`),console.log(" Email: "+u.email),console.log(" Account ID: "+u.accountId),i!==null&&console.log(" Token Balance: $"+i.toFixed(2)),n.project&&console.log(`
|
|
151
|
+
Default Project: `+n.project),console.log()}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}I();b();w();j();import{Command as mt}from"commander";function he(){let r=new mt("tokens");return r.description("View token balance and usage"),r.command("balance").description("Check remaining token/credit balance").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=new a({json:t.json,color:n.color}),s=new d(n),{balance:u}=await s.getTokenBalance();if(t.json){let i=a.result({balance:u});o.output(i)}else console.log(`
|
|
152
|
+
\u{1F4B0} Token Balance
|
|
153
|
+
`),console.log(` Current Balance: ${u.toLocaleString()} tokens`),console.log(` Estimated Tests: ~${Math.floor(u/100)} tests
|
|
154
|
+
`),console.log(`\u{1F4A1} Purchase more tokens: https://runhuman.com/billing
|
|
155
|
+
`)}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("history").description("View token usage history").option("--limit <number>","Number of records (default: 20)",parseInt).option("--offset <number>","Pagination offset (default: 0)",parseInt).option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=new a({json:t.json,color:n.color});if(t.json){let s=a.result({message:"Token history endpoint not yet available",history:[]});o.output(s)}else console.log(`
|
|
156
|
+
\u{1F4CA} Token Usage History
|
|
157
|
+
`),console.log(` This feature is coming soon!
|
|
158
|
+
`),console.log(" For now, view usage in the dashboard:"),console.log(` https://runhuman.com/dashboard/usage
|
|
159
|
+
`)}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}b();w();j();import{Command as gt}from"commander";function we(){let r=new gt("config");return r.description("Manage CLI configuration"),r.command("get").description("Get a configuration value").argument("<key>","Configuration key").option("--json","Output as JSON").action(async(t,e)=>{try{let o=await new m().get(t),s=new a({json:e.json});if(e.json){let u=a.result({[t]:o});s.output(u)}else console.log(o!==void 0?o:"(not set)")}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("set").description("Set a configuration value").argument("<key>","Configuration key").argument("<value>","Configuration value").option("--global","Set globally (not project-specific)").option("--json","Output as JSON").action(async(t,e,n)=>{try{await new m().set(t,e,n.global);let s=new a({json:n.json});if(n.json){let u=a.result({success:!0,key:t,value:e,scope:n.global?"global":"project"});s.output(u)}else s.success(`Set ${t} = ${e}`+(n.global?" (global)":" (project)"))}catch(o){let s=p(o);new a({json:n.json}).outputError(s.message,s.details),process.exit(s.exitCode)}}),r.command("list").description("List all configuration values").option("--show-secrets","Show API keys (default: masked)").option("--json","Output as JSON").action(async t=>{try{let n=await new m().list(),o=new a({json:t.json});if(t.json){let s=a.result(n);o.output(s)}else{if(console.log(`
|
|
160
|
+
\u2699\uFE0F Configuration
|
|
161
|
+
`),n.global&&Object.keys(n.global).length>0){console.log("Global (~/.config/runhuman/config.json):");for(let[s,u]of Object.entries(n.global)){let i=s==="apiKey"&&!t.showSecrets?ye(String(u)):u;console.log(` ${s}:`.padEnd(20)+i)}console.log()}if(n.project&&Object.keys(n.project).length>0){console.log("Project (.runhumanrc):");for(let[s,u]of Object.entries(n.project)){let i=s==="apiKey"&&!t.showSecrets?ye(String(u)):u;console.log(` ${s}:`.padEnd(20)+i)}console.log()}console.log("\u{1F4A1} Set value: runhuman config set <key> <value>"),console.log(` Get value: runhuman config get <key>
|
|
162
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("reset").description("Reset configuration to defaults").option("--global","Reset global config").option("--project","Reset project config").option("--all","Reset all config").option("--force","Skip confirmation prompt").option("--json","Output as JSON").action(async t=>{try{if(!t.global&&!t.project&&!t.all)throw new Error("Specify --global, --project, or --all");let e=t.all?"all":t.global?"global":"project";t.force||(console.log(`\u26A0\uFE0F This will reset ${e} configuration to defaults.`),console.log(`Use --force to skip this prompt.
|
|
163
|
+
`),process.exit(0)),await new m().reset(e);let o=new a({json:t.json});if(t.json){let s=a.result({success:!0,scope:e});o.output(s)}else o.success(`Reset ${e} configuration`)}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}function ye(r){return r.length<=8?"****":r.substring(0,4)+"*".repeat(r.length-8)+r.substring(r.length-4)}w();j();import{Command as dt}from"commander";import ft from"inquirer";import{writeFileSync as ht}from"fs";import{join as yt}from"path";function je(){let r=new dt("init");return r.description("Initialize a new Runhuman project with configuration").option("--name <text>","Project name").option("--url <url>","Default URL").option("--github-repo <owner/repo>","GitHub repository").option("--yes","Skip all prompts (use defaults)").option("--json","Output as JSON").action(async t=>{try{let e=new a({json:t.json});!t.json&&!t.yes&&(console.log(`\u{1F680} Welcome to Runhuman!
|
|
164
|
+
`),console.log("Let's set up your project. This will create:"),console.log(" \u2022 A configuration file (.runhumanrc)"),console.log(` \u2022 Setup your project defaults
|
|
165
|
+
`),console.log("=".repeat(60)+`
|
|
166
|
+
`));let n=t.name,o=t.url,s=t.githubRepo;if(!t.yes&&!t.json){let c=await ft.prompt([{type:"input",name:"projectName",message:"Project name:",default:"My Project",when:!n},{type:"input",name:"defaultUrl",message:"Default URL (optional):",when:!o},{type:"input",name:"githubRepo",message:"Link to GitHub repository (optional, e.g., owner/repo):",when:!s}]);n=n||c.projectName,o=o||c.defaultUrl,s=s||c.githubRepo}let u={defaultUrl:o||void 0,githubRepo:s||void 0,defaultDuration:5,defaultScreenSize:"desktop"},i=yt(process.cwd(),".runhumanrc");if(ht(i,JSON.stringify(u,null,2)),t.json){let c=a.result({success:!0,configPath:i,config:u});e.output(c)}else console.log(`
|
|
167
|
+
`+"=".repeat(60)),e.success("Project initialized successfully!"),console.log(`\u{1F4C1} Configuration saved to: .runhumanrc
|
|
168
|
+
`),console.log(`\u{1F389} All set! Try creating your first test:
|
|
169
|
+
`),console.log(` runhuman create https://example.com -d "Test homepage"
|
|
170
|
+
`),console.log(`\u{1F4DA} Learn more: https://runhuman.com/docs/cli
|
|
171
|
+
`)}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}I();b();w();j();import{Command as wt}from"commander";import jt from"cli-table3";function Ce(){let r=new wt("projects");return r.description("Manage projects"),r.command("list").alias("ls").description("List all projects").option("--limit <number>","Number of results (default: 20)",parseInt).option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=new a({json:t.json,color:n.color}),s=new d(n),{projects:u,total:i}=await s.listProjects({limit:t.limit||20});if(t.json){let c=a.result({projects:u,total:i});o.output(c)}else{console.log(`
|
|
172
|
+
\u{1F4C1} Your Projects (${u.length})
|
|
173
|
+
`);let c=new jt({head:["Project ID","Name","Created"].map(l=>l),colWidths:[20,30,20]});u.forEach(l=>{c.push([l.id,l.name,new Date(l.createdAt).toLocaleDateString()])}),console.log(c.toString()),console.log(`
|
|
174
|
+
\u{1F4A1} Set default project: runhuman config set project <id>
|
|
175
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("create").alias("new").description("Create a new project").argument("<name>","Project name").option("--description <text>","Project description").option("--default-url <url>","Default URL for tests").option("--github-repo <owner/repo>","Link to GitHub repository").option("--set-default","Set as default project").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let n=new m,o=await n.loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),i=await new d(o).createProject({name:t,description:e.description,defaultUrl:e.defaultUrl,githubRepo:e.githubRepo});if(e.setDefault&&await n.set("project",i.id,!1),e.json){let c=a.result(i);s.output(c)}else s.success("Project created successfully!"),console.log(" Project ID: "+i.id),console.log(" Name: "+i.name),e.setDefault&&console.log(" Set as default project"),console.log()}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("show").alias("info").alias("get").description("Show detailed project information").argument("<projectId>","Project ID to show").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),i=await new d(o).getProject(t);if(e.json){let c=a.result(i);s.output(c)}else console.log(`
|
|
176
|
+
\u{1F4C1} Project Details
|
|
177
|
+
`),console.log(" Project ID: "+i.id),console.log(" Name: "+i.name),i.description&&console.log(" Description: "+i.description),i.defaultUrl&&console.log(" Default URL: "+i.defaultUrl),i.githubRepo&&console.log(" GitHub Repo: "+i.githubRepo),console.log(" Created: "+new Date(i.createdAt).toLocaleString()),console.log()}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("update").description("Update project settings").argument("<projectId>","Project ID to update").option("--name <text>","Update name").option("--description <text>","Update description").option("--default-url <url>","Update default URL").option("--github-repo <owner/repo>","Update GitHub repo").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),u={name:e.name,description:e.description,defaultUrl:e.defaultUrl,githubRepo:e.githubRepo},c=await new d(o).updateProject(t,u);if(e.json){let l=a.result(c);s.output(l)}else s.success("Project updated successfully!")}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("delete").alias("rm").description("Delete a project permanently").argument("<projectId>","Project ID to delete").option("--force","Skip confirmation prompt").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{e.force||(console.log("\u26A0\uFE0F This will permanently delete the project and all its data."),console.log(`Use --force to skip this prompt.
|
|
178
|
+
`),process.exit(0));let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color});if(await new d(o).deleteProject(t),e.json){let i=a.result({success:!0});s.output(i)}else s.success("Project deleted successfully!")}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}I();b();w();j();import{Command as Ct}from"commander";import bt from"cli-table3";function be(){let r=new Ct("keys");return r.description("Manage API keys"),r.command("list").alias("ls").description("List all API keys").option("--project <id>","Filter by project (required)").option("--show-keys","Show full API keys (default: masked)").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=t.project||n.project;if(!o)throw new Error("Project ID required (use --project or set default project)");let s=new a({json:t.json,color:n.color}),i=await new d(n).listApiKeys(o);if(t.json){let c=a.result({keys:i});s.output(c)}else{console.log(`
|
|
179
|
+
\u{1F511} API Keys (${i.length})
|
|
180
|
+
`);let c=new bt({head:["Key ID","Name","Key","Last Used","Created"].map(l=>l),colWidths:[20,25,20,20,20]});i.forEach(l=>{let g=t.showKeys?l.key:Ut(l.key),f=l.lastUsedAt?new Date(l.lastUsedAt).toLocaleDateString():"Never";c.push([l.id,l.name,g,f,new Date(l.createdAt).toLocaleDateString()])}),console.log(c.toString()),console.log(`
|
|
181
|
+
\u{1F4A1} Create new key: runhuman keys create "Key Name" --project <id>`),console.log(` Show full key: runhuman keys show <keyId>
|
|
182
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("create").alias("new").description("Create a new API key").argument("<name>","API key name").option("--project <id>","Project ID (required)").option("--copy","Copy key to clipboard").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=e.project||o.project;if(!s)throw new Error("Project ID required (use --project or set default project)");let u=new a({json:e.json,color:o.color}),c=await new d(o).createApiKey(s,t);if(e.json){let l=a.result(c);u.output(l)}else u.success("API Key created successfully!"),console.log(`
|
|
183
|
+
Key ID: `+c.id),console.log(" Name: "+c.name),console.log(`
|
|
184
|
+
API Key: `+c.key),console.log(" "+"^".repeat(c.key.length)),console.log(` \u26A0\uFE0F Save this key! It won't be shown again.
|
|
185
|
+
`),console.log("\u{1F4A1} Use this key:"),console.log(" export RUNHUMAN_API_KEY="+c.key),console.log(` runhuman create https://myapp.com -d "Test"
|
|
186
|
+
`),console.log("\u{1F512} Store securely:"),console.log(" - Use environment variables (recommended)"),console.log(" - Use secret management tools"),console.log(` - Never commit to git!
|
|
187
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("show").alias("info").alias("get").description("Show details of a specific API key").argument("<keyId>","Key ID to show").option("--show-key","Show full API key (default: masked)").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl});new a({json:e.json,color:o.color}).warn("Note: keys show requires listing all keys first"),console.log(`Use: runhuman keys list --show-keys
|
|
188
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("delete").aliases(["rm","revoke"]).description("Delete an API key permanently").argument("<keyId>","Key ID to delete").option("--force","Skip confirmation prompt").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{e.force||(console.log("\u26A0\uFE0F This will permanently delete the API key."),console.log(`Use --force to skip this prompt.
|
|
189
|
+
`),process.exit(0));let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color});if(await new d(o).deleteApiKey(t),e.json){let i=a.result({success:!0});s.output(i)}else s.success("API key deleted successfully!")}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}function Ut(r){return r.length<=12?"****":r.substring(0,8)+"*".repeat(r.length-12)+r.substring(r.length-4)}I();b();w();j();import{Command as kt}from"commander";import It from"cli-table3";function Ue(){let r=new kt("templates");return r.description("Manage test templates"),r.command("list").alias("ls").description("List all test templates").option("--project <id>","Filter by project (required)").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=t.project||n.project;if(!o)throw new Error("Project ID required (use --project or set default project)");let s=new a({json:t.json,color:n.color}),i=await new d(n).listTemplates(o);if(t.json){let c=a.result({templates:i});s.output(c)}else{console.log(`
|
|
190
|
+
\u{1F4CB} Test Templates (${i.length})
|
|
191
|
+
`);let c=new It({head:["ID","Name","Description","Created"].map(l=>l),colWidths:[30,25,35,20]});i.forEach(l=>{let g=l.description&&l.description.length>30?l.description.substring(0,27)+"...":l.description||"-";c.push([l.id,l.name,g,new Date(l.createdAt).toLocaleDateString()])}),console.log(c.toString()),console.log(`
|
|
192
|
+
\u{1F4A1} Create new template: runhuman templates create "Template Name" --project <id>`),console.log(` View template: runhuman templates show <templateId>
|
|
193
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("create").alias("new").description("Create a new test template").argument("<name>","Template name").option("--project <id>","Project ID (required)").option("-d, --description <text>","Template description").option("--duration <seconds>","Target test duration in seconds").option("--screen-size <size>","Default screen size (desktop/mobile/tablet)").option("--schema <path>","Path to JSON schema file").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=e.project||o.project;if(!s)throw new Error("Project ID required (use --project or set default project)");let u=new a({json:e.json,color:o.color}),i;if(e.schema){let{readFileSync:f}=await import("fs"),h=f(e.schema,"utf-8");i=JSON.parse(h)}let c={name:t,description:e.description,targetDurationMinutes:e.duration?Math.floor(parseInt(e.duration)/60):void 0,defaultScreenSize:e.screenSize,outputSchema:i},g=await new d(o).createTemplate(s,c);if(e.json){let f=a.result(g);u.output(f)}else u.success("Template created successfully!"),console.log(`
|
|
194
|
+
Template ID: `+g.id),console.log(" Name: "+g.name),g.description&&console.log(" Description: "+g.description),console.log(`
|
|
195
|
+
\u{1F4A1} Use this template:`),console.log(" runhuman create https://myapp.com --template "+g.id+`
|
|
196
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("show").alias("info").alias("get").description("Show details of a specific template").argument("<templateId>","Template ID to show").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),i=await new d(o).getTemplate(t);if(e.json){let c=a.result(i);s.output(c)}else console.log(`
|
|
197
|
+
\u{1F4CB} Template Details
|
|
198
|
+
`),console.log("ID: "+i.id),console.log("Name: "+i.name),console.log("Description: "+(i.description||"-")),console.log("Project: "+i.projectId),i.targetDurationMinutes&&console.log("Duration: "+i.targetDurationMinutes+" minutes"),i.defaultScreenSize&&console.log("Screen Size: "+i.defaultScreenSize),console.log("Created: "+new Date(i.createdAt).toLocaleString()),i.outputSchema&&(console.log(`
|
|
199
|
+
Output Schema:`),console.log(JSON.stringify(i.outputSchema,null,2))),console.log(`
|
|
200
|
+
\u{1F4A1} Use this template:`),console.log(" runhuman create https://myapp.com --template "+i.id+`
|
|
201
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("update").alias("edit").description("Update a template").argument("<templateId>","Template ID to update").option("--name <name>","New template name").option("-d, --description <text>","New description").option("--duration <seconds>","New target duration in seconds").option("--screen-size <size>","New default screen size").option("--schema <path>","Path to new JSON schema file").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),u;if(e.schema){let{readFileSync:f}=await import("fs"),h=f(e.schema,"utf-8");u=JSON.parse(h)}let i={name:e.name,description:e.description,targetDurationMinutes:e.duration?Math.floor(parseInt(e.duration)/60):void 0,defaultScreenSize:e.screenSize,outputSchema:u},c=Object.fromEntries(Object.entries(i).filter(([,f])=>f!==void 0));if(Object.keys(c).length===0)throw new Error("No updates provided. Use --name, --description, --duration, --screen-size, or --schema");let g=await new d(o).updateTemplate(t,i);if(e.json){let f=a.result(g);s.output(f)}else s.success("Template updated successfully!"),console.log(`
|
|
202
|
+
Template ID: `+g.id),console.log(" Name: "+g.name+`
|
|
203
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("delete").alias("rm").description("Delete a template permanently").argument("<templateId>","Template ID to delete").option("--force","Skip confirmation prompt").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{e.force||(console.log("\u26A0\uFE0F This will permanently delete the template."),console.log(`Use --force to skip this prompt.
|
|
204
|
+
`),process.exit(0));let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color});if(await new d(o).deleteTemplate(t),e.json){let i=a.result({success:!0});s.output(i)}else s.success("Template deleted successfully!")}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r}I();b();w();j();import{Command as At}from"commander";import ke from"cli-table3";function Ie(){let r=new At("github");return r.alias("gh"),r.description("GitHub integration commands"),r.command("link").description("Link a GitHub repository to your project").argument("<repo>","Repository in format owner/repo").option("--project <id>","Project ID (required)").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let n=new m,o=await n.loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=e.project||o.project;if(!s)throw new Error("Project ID required (use --project or set default project)");let u=new a({json:e.json,color:o.color}),i=t.match(/^([^/]+)\/([^/]+)$/);if(!i)throw new Error("Invalid repository format. Use: owner/repo");let[,c,l]=i,f=await new d(o).linkGithubRepo(s,c,l);if(e.json){let h=a.result(f);u.output(h)}else u.success("GitHub repository linked successfully!"),console.log(`
|
|
205
|
+
Repository: `+t),console.log(" Project: "+s),console.log(`
|
|
206
|
+
\u{1F4A1} Now you can:`),console.log(" - List issues: runhuman github issues "+t),console.log(" - Test an issue: runhuman github test <issueNumber> --repo "+t),console.log(" - Bulk test: runhuman github bulk-test --repo "+t+`
|
|
207
|
+
`),await n.saveProjectConfig({githubRepo:t}),console.log(`\u2713 Repository saved to project config (.runhumanrc)
|
|
208
|
+
`)}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("repos").alias("repositories").description("List linked GitHub repositories").option("--project <id>","Filter by project").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=t.project||n.project,s=new a({json:t.json,color:n.color}),i=await new d(n).listGithubRepos(o);if(t.json){let c=a.result({repositories:i});s.output(c)}else{console.log(`
|
|
209
|
+
\u{1F517} Linked GitHub Repositories (${i.length})
|
|
210
|
+
`);let c=new ke({head:["Repository","Project","Linked"].map(l=>l),colWidths:[35,30,20]});i.forEach(l=>{c.push([l.fullName,l.projectId,new Date(l.linkedAt).toLocaleDateString()])}),console.log(c.toString()),console.log(`
|
|
211
|
+
\u{1F4A1} Test an issue: runhuman github test <issueNumber> --repo owner/repo
|
|
212
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r.command("issues").description("List GitHub issues for a repository").argument("<repo>","Repository in format owner/repo").option("--state <state>","Filter by state (open/closed/all)","open").option("--labels <labels>","Filter by comma-separated labels").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=new a({json:e.json,color:o.color}),u=t.match(/^([^/]+)\/([^/]+)$/);if(!u)throw new Error("Invalid repository format. Use: owner/repo");let[,i,c]=u,g=await new d(o).listGithubIssues(i,c,{state:e.state,labels:e.labels?.split(",")});if(e.json){let f=a.result({issues:g});s.output(f)}else{console.log(`
|
|
213
|
+
\u{1F41B} GitHub Issues for ${t} (${g.length})
|
|
214
|
+
`);let f=new ke({head:["#","Title","State","Labels","Created"].map(h=>h),colWidths:[8,40,10,20,15]});g.forEach(h=>{let U=h.labels?.join(", ")||"-",y=U.length>18?U.substring(0,15)+"...":U;f.push(["#"+h.number,h.title.length>38?h.title.substring(0,35)+"...":h.title,h.state,y,new Date(h.createdAt).toLocaleDateString()])}),console.log(f.toString()),console.log(`
|
|
215
|
+
\u{1F4A1} Test an issue: runhuman github test <issueNumber> --repo `+t+`
|
|
216
|
+
`)}}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("test").description("Create a QA test job for a GitHub issue").argument("<issueNumber>","Issue number to test").option("--repo <owner/repo>","Repository (or use default from config)").option("--url <url>","URL to test (required)").option("--template <id>","Template ID to use").option("--sync","Wait for result before exiting").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async(t,e)=>{try{let o=await new m().loadConfig({apiKey:e.apiKey,apiUrl:e.apiUrl}),s=e.repo||o.githubRepo;if(!s)throw new Error("Repository required (use --repo or set default with: runhuman github link)");if(!e.url)throw new Error("URL required (use --url)");let u=new a({json:e.json,color:o.color}),i=s.match(/^([^/]+)\/([^/]+)$/);if(!i)throw new Error("Invalid repository format. Use: owner/repo");let[,c,l]=i,g=new d(o),f=await g.getGithubIssue(c,l,parseInt(t)),h={url:e.url,description:`Test GitHub issue #${t}: ${f.title}
|
|
217
|
+
|
|
218
|
+
${f.body}`,metadata:{githubIssue:{owner:c,repo:l,number:parseInt(t),url:f.url}},templateId:e.template},U=await g.createJob(h);if(e.json){let y=a.result(U);u.output(y)}else if(u.success("QA test job created for issue #"+t),console.log(`
|
|
219
|
+
Job ID: `+U.jobId),console.log(" Issue: #"+t+" - "+f.title),console.log(" Status: "+U.status),console.log(" URL: "+e.url),console.log(`
|
|
220
|
+
\u{1F4A1} Check status: runhuman status `+U.jobId+`
|
|
221
|
+
`),e.sync){let{waitForJob:y}=await Promise.resolve().then(()=>(W(),X));await y(U.jobId,g,u,600)}}catch(n){let o=p(n);new a({json:e.json}).outputError(o.message,o.details),process.exit(o.exitCode)}}),r.command("bulk-test").description("Create QA test jobs for multiple GitHub issues").option("--repo <owner/repo>","Repository (or use default from config)").option("--url <url>","URL to test (required)").option("--labels <labels>","Filter issues by comma-separated labels").option("--state <state>","Filter by state (open/closed/all)","open").option("--template <id>","Template ID to use").option("--limit <number>","Maximum number of jobs to create","10").option("--json","Output as JSON").option("--api-key <key>","API key").option("--api-url <url>","API URL").action(async t=>{try{let n=await new m().loadConfig({apiKey:t.apiKey,apiUrl:t.apiUrl}),o=t.repo||n.githubRepo;if(!o)throw new Error("Repository required (use --repo or set default with: runhuman github link)");if(!t.url)throw new Error("URL required (use --url)");let s=new a({json:t.json,color:n.color}),u=o.match(/^([^/]+)\/([^/]+)$/);if(!u)throw new Error("Invalid repository format. Use: owner/repo");let[,i,c]=u,l=new d(n),g=await l.listGithubIssues(i,c,{state:t.state,labels:t.labels?.split(",")}),f=parseInt(t.limit),h=g.slice(0,f);if(h.length===0){console.log(`No issues found matching the criteria.
|
|
222
|
+
`);return}console.log(`
|
|
223
|
+
\u{1F680} Creating ${h.length} test jobs...
|
|
224
|
+
`);let U=[];for(let y of h){let D={url:t.url,description:`Test GitHub issue #${y.number}: ${y.title}
|
|
225
|
+
|
|
226
|
+
${y.body}`,metadata:{githubIssue:{owner:i,repo:c,number:y.number,url:y.url}},templateId:t.template};try{let P=await l.createJob(D);U.push({issue:y.number,jobId:P.jobId,status:"created"}),console.log(` \u2713 Issue #${y.number} \u2192 Job ${P.jobId}`)}catch(P){let K=p(P);U.push({issue:y.number,error:K.message,status:"failed"}),console.log(` \u2717 Issue #${y.number} \u2192 Failed: ${K.message}`)}}if(t.json){let y=a.result({jobs:U});s.output(y)}else{let y=U.filter(P=>P.status==="created").length,D=U.filter(P=>P.status==="failed").length;console.log(`
|
|
227
|
+
\u2713 Created ${y} jobs`),D>0&&console.log(`\u2717 Failed ${D} jobs`),console.log(`
|
|
228
|
+
\u{1F4A1} Check all jobs: runhuman list
|
|
229
|
+
`)}}catch(e){let n=p(e);new a({json:t.json}).outputError(n.message,n.details),process.exit(n.exitCode)}}),r}b();I();import{existsSync as Pt}from"fs";import{join as St}from"path";import{execSync as Rt}from"child_process";import M from"inquirer";import k from"chalk";import Pe from"ora";j();async function Se(){let r=new m,t=new a({color:!0});console.log(""),console.log(k.bold("Runhuman")+" - Human QA testing for your apps"),console.log("");let e=await Ae(r);if(!e.isAuthenticated){let{shouldLogin:n}=await M.prompt([{type:"confirm",name:"shouldLogin",message:"You're not logged in. Would you like to sign in?",default:!0}]);if(!n){console.log(`
|
|
230
|
+
Run `+k.cyan("runhuman login")+` when you're ready to sign in.
|
|
231
|
+
`);return}await xt(r,t),Object.assign(e,await Ae(r))}console.log(k.green("Logged in as "+e.userEmail)+`
|
|
232
|
+
`),e.isRunhumanProject?await Ot(r,e):await Dt(r,e)}async function Ae(r){let t={isAuthenticated:!1,isGitRepo:!1,isRunhumanProject:!1},e=r.loadCredentials();if(e?.accessToken)try{let o=await r.loadConfig({apiKey:e.accessToken}),u=await new d(o).getCurrentUser();t.isAuthenticated=!0,t.userEmail=u.email}catch{t.isAuthenticated=!1}try{Rt("git rev-parse --is-inside-work-tree",{stdio:"ignore"}),t.isGitRepo=!0}catch{t.isGitRepo=!1}let n=St(process.cwd(),".runhumanrc");if(Pt(n)){t.isRunhumanProject=!0;try{let o=await r.loadConfig();t.projectConfig={defaultUrl:o.defaultUrl,githubRepo:o.githubRepo}}catch{}}return t}async function xt(r,t){let e=await r.loadConfig(),n=e.apiUrl||"https://runhuman.com";console.log("");let o=Pe("Opening browser for authentication...").start();try{let s=await V({apiUrl:n,autoOpenBrowser:e.autoOpenBrowser!==!1});o.stop(),r.saveCredentials({accessToken:s.token});let u=await r.loadConfig({apiKey:s.token}),c=await new d(u).getCurrentUser();r.saveUserInfo(c),t.success("Successfully logged in!"),console.log("")}catch(s){throw o.stop(),s}}async function Ot(r,t){console.log(k.dim("Runhuman project detected")),t.projectConfig?.defaultUrl&&console.log(k.dim("URL: "+t.projectConfig.defaultUrl)),console.log("");let{action:e}=await M.prompt([{type:"list",name:"action",message:"What would you like to do?",choices:[{name:"Quick test a URL",value:"quick-test"},{name:"Run a template",value:"run-template"},{name:"View recent jobs",value:"list-jobs"},new M.Separator,{name:"Exit",value:"exit"}]}]);switch(e){case"quick-test":await Re(r,t);break;case"run-template":console.log(`
|
|
233
|
+
Run `+k.cyan("runhuman templates")+" to see available templates."),console.log("Then run "+k.cyan("runhuman create --template <name>")+` to use one.
|
|
234
|
+
`);break;case"list-jobs":console.log(`
|
|
235
|
+
Run `+k.cyan("runhuman list")+` to see your recent jobs.
|
|
236
|
+
`);break;case"exit":break}}async function Dt(r,t){t.isGitRepo?console.log(k.dim("Git repository detected (not yet set up with Runhuman)")):console.log(k.dim("Not in a git repository")),console.log("");let{action:e}=await M.prompt([{type:"list",name:"action",message:"What would you like to do?",choices:[{name:"Quick test a URL",value:"quick-test"},{name:"Set up this repo with Runhuman",value:"setup-repo",disabled:!t.isGitRepo},{name:"Connect a GitHub repo",value:"connect-github"},new M.Separator,{name:"Exit",value:"exit"}]}]);switch(e){case"quick-test":await Re(r,t);break;case"setup-repo":await Et(r);break;case"connect-github":await Nt(r);break;case"exit":break}}async function Re(r,t){let e=t.projectConfig?.defaultUrl||"",n=await M.prompt([{type:"input",name:"url",message:"URL to test:",default:e||void 0,validate:s=>{if(!s.trim())return"URL is required";try{return new URL(s),!0}catch{return"Please enter a valid URL"}}},{type:"input",name:"description",message:"What should we test? (describe in plain English):",validate:s=>s.trim()?!0:"Description is required"}]),o=Pe("Creating test job...").start();try{let s=r.loadCredentials(),u=await r.loadConfig({apiKey:s?.accessToken}),c=await new d(u).createJob({url:n.url,description:n.description});o.succeed("Test job created!"),console.log(""),console.log(" Job ID: "+k.cyan(c.jobId)),console.log(" Status: "+c.status),console.log(""),console.log("A human tester will test your app shortly."),console.log("Run "+k.cyan(`runhuman wait ${c.jobId}`)+" to wait for results."),console.log("")}catch(s){throw o.fail("Failed to create test job"),s}}async function Et(r){console.log(""),console.log("Setting up Runhuman in the current repository..."),console.log("");let t=await M.prompt([{type:"input",name:"defaultUrl",message:"Default URL to test (optional):"}]);await r.saveProjectConfig({defaultUrl:t.defaultUrl||void 0,defaultDuration:5,defaultScreenSize:"desktop"}),console.log(""),console.log(k.green("Created .runhumanrc")),console.log(""),console.log("Next steps:"),console.log(" 1. Install the GitHub App to enable @runhuman comments"),console.log(" "+k.cyan("runhuman github connect")),console.log(""),console.log(" 2. Create your first test:"),console.log(" "+k.cyan('runhuman create https://your-app.com -d "Test login flow"')),console.log("")}async function Nt(r){let n=`${(await r.loadConfig()).apiUrl||"https://runhuman.com"}/dashboard/settings/github`;console.log(""),console.log("To connect your GitHub repos, install the Runhuman GitHub App:"),console.log(""),console.log(" "+k.cyan(n)),console.log(""),console.log("After installation, you can comment "+k.bold("@runhuman")+" on any issue"),console.log("to trigger a QA test."),console.log("")}import{readFileSync as Tt}from"fs";import{join as Mt,dirname as Jt}from"path";import{fileURLToPath as $t}from"url";var Lt=$t(import.meta.url),Kt=Jt(Lt),Ft=Mt(Kt,"../package.json"),Ht=JSON.parse(Tt(Ft,"utf-8")),Gt=Ht.version,C=new vt;C.name("runhuman").description("CLI for Runhuman - AI-orchestrated human QA testing").version(Gt);C.addCommand(ne());C.addCommand(re());C.addCommand(Z());C.addCommand(se());C.addCommand(ie());C.addCommand(ae());C.addCommand(ue());C.addCommand(ge());C.addCommand(de());C.addCommand(fe());C.addCommand(he());C.addCommand(we());C.addCommand(je());C.addCommand(Ce());C.addCommand(be());C.addCommand(Ue());C.addCommand(Ie());process.argv.slice(2).length?C.parse(process.argv):Se().catch(r=>{console.error(r.message),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runhuman",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI for Runhuman - AI-orchestrated human QA testing
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for Runhuman - AI-orchestrated human QA testing",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"runhuman": "index.js"
|
|
8
|
+
"runhuman": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"index.js",
|
|
12
|
-
"README.md"
|
|
11
|
+
"dist/index.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
13
14
|
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"registry": "https://registry.npmjs.org/"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"build:dev": "tsc",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
|
|
24
|
+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --watch",
|
|
25
|
+
"test:coverage": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --coverage",
|
|
26
|
+
"test:unit": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest tests/unit",
|
|
27
|
+
"test:integration": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest tests/integration",
|
|
28
|
+
"type-check": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
14
31
|
"keywords": [
|
|
15
32
|
"runhuman",
|
|
16
33
|
"qa",
|
|
@@ -18,7 +35,9 @@
|
|
|
18
35
|
"human-in-the-loop",
|
|
19
36
|
"cli",
|
|
20
37
|
"qa-testing",
|
|
21
|
-
"manual-testing"
|
|
38
|
+
"manual-testing",
|
|
39
|
+
"test-automation",
|
|
40
|
+
"quality-assurance"
|
|
22
41
|
],
|
|
23
42
|
"author": "Runhuman <hey@runhuman.com>",
|
|
24
43
|
"repository": {
|
|
@@ -33,5 +52,28 @@
|
|
|
33
52
|
"license": "ISC",
|
|
34
53
|
"engines": {
|
|
35
54
|
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@runhuman/shared": "*",
|
|
58
|
+
"axios": "^1.7.9",
|
|
59
|
+
"boxen": "^8.0.1",
|
|
60
|
+
"chalk": "^5.3.0",
|
|
61
|
+
"chokidar": "^5.0.0",
|
|
62
|
+
"cli-table3": "^0.6.5",
|
|
63
|
+
"commander": "^12.1.0",
|
|
64
|
+
"cosmiconfig": "^9.0.0",
|
|
65
|
+
"inquirer": "^10.2.2",
|
|
66
|
+
"open": "^10.1.0",
|
|
67
|
+
"ora": "^8.1.0",
|
|
68
|
+
"zod": "^3.23.8"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@jest/globals": "^29.7.0",
|
|
72
|
+
"@types/inquirer": "^9.0.7",
|
|
73
|
+
"@types/node": "^22.10.2",
|
|
74
|
+
"jest": "^29.7.0",
|
|
75
|
+
"ts-jest": "^29.2.5",
|
|
76
|
+
"tsup": "^8.3.5",
|
|
77
|
+
"typescript": "^5.7.2"
|
|
36
78
|
}
|
|
37
79
|
}
|
package/index.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Runhuman CLI - Coming Soon
|
|
5
|
-
*
|
|
6
|
-
* This is a placeholder package that reserves the `runhuman` npm package name.
|
|
7
|
-
* The full CLI is currently in development.
|
|
8
|
-
*
|
|
9
|
-
* For now, if you're looking to use Runhuman with AI agents (Claude, GPT, etc.),
|
|
10
|
-
* please use the MCP server package instead:
|
|
11
|
-
*
|
|
12
|
-
* npm install -g @runhuman/mcp
|
|
13
|
-
*
|
|
14
|
-
* Or use with npx:
|
|
15
|
-
*
|
|
16
|
-
* npx @runhuman/mcp --api-key=qa_live_xxxxx
|
|
17
|
-
*
|
|
18
|
-
* Documentation: https://runhuman.com
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
console.log(`
|
|
22
|
-
╔═══════════════════════════════════════════════════════════════════════════╗
|
|
23
|
-
║ ║
|
|
24
|
-
║ 🚧 Runhuman CLI - Coming Soon 🚧 ║
|
|
25
|
-
║ ║
|
|
26
|
-
╚═══════════════════════════════════════════════════════════════════════════╝
|
|
27
|
-
|
|
28
|
-
The Runhuman CLI is currently under development.
|
|
29
|
-
|
|
30
|
-
For AI agents (Claude, GPT, etc.), use the MCP server:
|
|
31
|
-
|
|
32
|
-
📦 Install: npm install -g @runhuman/mcp
|
|
33
|
-
🚀 Run: npx @runhuman/mcp --api-key=qa_live_xxxxx
|
|
34
|
-
|
|
35
|
-
For Claude Desktop, add to your config:
|
|
36
|
-
{
|
|
37
|
-
"mcpServers": {
|
|
38
|
-
"runhuman": {
|
|
39
|
-
"command": "npx",
|
|
40
|
-
"args": ["-y", "@runhuman/mcp", "--api-key=YOUR_KEY"]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
📚 Documentation: https://runhuman.com
|
|
46
|
-
🔑 Get API Key: https://runhuman.com/dashboard
|
|
47
|
-
💬 Support: hey@runhuman.com
|
|
48
|
-
|
|
49
|
-
The full CLI with commands like 'runhuman create', 'runhuman list', etc.
|
|
50
|
-
will be available soon!
|
|
51
|
-
`);
|
|
52
|
-
|
|
53
|
-
process.exit(0);
|