@vertaaux/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -0
- package/dist/auth/ci-token.d.ts +49 -0
- package/dist/auth/ci-token.d.ts.map +1 -0
- package/dist/auth/ci-token.js +83 -0
- package/dist/auth/device-flow.d.ts +66 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +156 -0
- package/dist/auth/token-store.d.ts +53 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +78 -0
- package/dist/baseline/diff.d.ts +57 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +152 -0
- package/dist/baseline/hash.d.ts +54 -0
- package/dist/baseline/hash.d.ts.map +1 -0
- package/dist/baseline/hash.js +66 -0
- package/dist/baseline/manager.d.ts +89 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +157 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +7 -0
- package/dist/cache/route-cache.d.ts +119 -0
- package/dist/cache/route-cache.d.ts.map +1 -0
- package/dist/cache/route-cache.js +213 -0
- package/dist/ci/changed-routes.d.ts +95 -0
- package/dist/ci/changed-routes.d.ts.map +1 -0
- package/dist/ci/changed-routes.js +304 -0
- package/dist/ci/github-api.d.ts +68 -0
- package/dist/ci/github-api.d.ts.map +1 -0
- package/dist/ci/github-api.js +138 -0
- package/dist/ci/gitlab-api.d.ts +75 -0
- package/dist/ci/gitlab-api.d.ts.map +1 -0
- package/dist/ci/gitlab-api.js +180 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/commands/audit.d.ts +58 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +862 -0
- package/dist/commands/baseline.d.ts +22 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +210 -0
- package/dist/commands/comment.d.ts +14 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +363 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +196 -0
- package/dist/commands/doctor.d.ts +58 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +338 -0
- package/dist/commands/download.d.ts +12 -0
- package/dist/commands/download.d.ts.map +1 -0
- package/dist/commands/download.js +183 -0
- package/dist/commands/explain.d.ts +62 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +302 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +212 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +222 -0
- package/dist/commands/policy.d.ts +13 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +347 -0
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +158 -0
- package/dist/config/defaults.d.ts +21 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +49 -0
- package/dist/config/loader.d.ts +66 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +167 -0
- package/dist/config/schema.d.ts +55 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/interactive/fix-wizard.d.ts +44 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -0
- package/dist/interactive/fix-wizard.js +286 -0
- package/dist/interactive/init-wizard.d.ts +32 -0
- package/dist/interactive/init-wizard.d.ts.map +1 -0
- package/dist/interactive/init-wizard.js +193 -0
- package/dist/interactive/prompts.d.ts +62 -0
- package/dist/interactive/prompts.d.ts.map +1 -0
- package/dist/interactive/prompts.js +78 -0
- package/dist/monorepo/detector.d.ts +70 -0
- package/dist/monorepo/detector.d.ts.map +1 -0
- package/dist/monorepo/detector.js +278 -0
- package/dist/monorepo/index.d.ts +9 -0
- package/dist/monorepo/index.d.ts.map +1 -0
- package/dist/monorepo/index.js +8 -0
- package/dist/monorepo/workspace.d.ts +142 -0
- package/dist/monorepo/workspace.d.ts.map +1 -0
- package/dist/monorepo/workspace.js +171 -0
- package/dist/output/envelope.d.ts +21 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +27 -0
- package/dist/output/factory.d.ts +73 -0
- package/dist/output/factory.d.ts.map +1 -0
- package/dist/output/factory.js +60 -0
- package/dist/output/formats.d.ts +11 -0
- package/dist/output/formats.d.ts.map +1 -0
- package/dist/output/formats.js +41 -0
- package/dist/output/html.d.ts +45 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/html.js +607 -0
- package/dist/output/human.d.ts +41 -0
- package/dist/output/human.d.ts.map +1 -0
- package/dist/output/human.js +274 -0
- package/dist/output/json.d.ts +42 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +37 -0
- package/dist/output/junit.d.ts +56 -0
- package/dist/output/junit.d.ts.map +1 -0
- package/dist/output/junit.js +135 -0
- package/dist/output/markdown.d.ts +77 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +411 -0
- package/dist/output/sarif.d.ts +160 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +207 -0
- package/dist/policy/evaluator.d.ts +111 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +362 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +11 -0
- package/dist/policy/loader.d.ts +97 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +281 -0
- package/dist/policy/schema.d.ts +297 -0
- package/dist/policy/schema.d.ts.map +1 -0
- package/dist/policy/schema.js +230 -0
- package/dist/quality-gate/evaluator.d.ts +58 -0
- package/dist/quality-gate/evaluator.d.ts.map +1 -0
- package/dist/quality-gate/evaluator.js +274 -0
- package/dist/quality-gate/index.d.ts +10 -0
- package/dist/quality-gate/index.d.ts.map +1 -0
- package/dist/quality-gate/index.js +7 -0
- package/dist/quality-gate/types.d.ts +103 -0
- package/dist/quality-gate/types.d.ts.map +1 -0
- package/dist/quality-gate/types.js +23 -0
- package/dist/templates/azure-devops.d.ts +25 -0
- package/dist/templates/azure-devops.d.ts.map +1 -0
- package/dist/templates/azure-devops.js +109 -0
- package/dist/templates/circleci.d.ts +28 -0
- package/dist/templates/circleci.d.ts.map +1 -0
- package/dist/templates/circleci.js +86 -0
- package/dist/templates/github-actions.d.ts +81 -0
- package/dist/templates/github-actions.d.ts.map +1 -0
- package/dist/templates/github-actions.js +393 -0
- package/dist/templates/gitlab-ci.d.ts +26 -0
- package/dist/templates/gitlab-ci.d.ts.map +1 -0
- package/dist/templates/gitlab-ci.js +70 -0
- package/dist/templates/index.d.ts +72 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +112 -0
- package/dist/templates/jenkins.d.ts +26 -0
- package/dist/templates/jenkins.d.ts.map +1 -0
- package/dist/templates/jenkins.js +110 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/diagnostics.d.ts +39 -0
- package/dist/ui/diagnostics.d.ts.map +1 -0
- package/dist/ui/diagnostics.js +153 -0
- package/dist/ui/spinner.d.ts +61 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +101 -0
- package/dist/ui/table.d.ts +63 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +236 -0
- package/dist/utils/client.d.ts +82 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +128 -0
- package/dist/utils/detect-env.d.ts +59 -0
- package/dist/utils/detect-env.d.ts.map +1 -0
- package/dist/utils/detect-env.js +115 -0
- package/dist/utils/exit-codes.d.ts +47 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +61 -0
- package/dist/utils/logger.d.ts +87 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +185 -0
- package/dist/utils/sanitize.d.ts +36 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +64 -0
- package/dist/utils/validators.d.ts +41 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +123 -0
- package/package.json +63 -0
- package/schemas/vertaaux.config.schema.json +103 -0
package/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# VertaaUX CLI
|
|
2
|
+
|
|
3
|
+
Run UX and accessibility audits from the terminal or CI pipelines.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @vertaaux/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @vertaaux/cli --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Authenticate
|
|
21
|
+
vertaa login
|
|
22
|
+
|
|
23
|
+
# 2. Run an audit
|
|
24
|
+
vertaa audit https://example.com --wait
|
|
25
|
+
|
|
26
|
+
# 3. Check CLI health
|
|
27
|
+
vertaa doctor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
The CLI checks for credentials in this order:
|
|
33
|
+
|
|
34
|
+
1. **Environment variables** (checked in order):
|
|
35
|
+
- `VERTAAUX_TOKEN`
|
|
36
|
+
- `VERTAAUX_API_KEY`
|
|
37
|
+
|
|
38
|
+
2. **Stored credentials** from interactive login:
|
|
39
|
+
```bash
|
|
40
|
+
vertaa login
|
|
41
|
+
```
|
|
42
|
+
Credentials are stored in `~/.vertaaux/credentials.json`.
|
|
43
|
+
|
|
44
|
+
3. **Direct token** for CI/non-interactive use:
|
|
45
|
+
```bash
|
|
46
|
+
vertaa login --token <api-key>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Verify authentication:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
vertaa whoami
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
### Core Commands
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `audit <url>` | Run UX and accessibility audit |
|
|
62
|
+
| `baseline [job-id]` | Create or update audit baseline |
|
|
63
|
+
| `diff` | Compare current audit against baseline |
|
|
64
|
+
| `policy init\|validate\|show\|schema` | Manage policy-as-code |
|
|
65
|
+
|
|
66
|
+
### Analysis and Remediation
|
|
67
|
+
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `explain <finding-id>` | Show evidence bundle for a finding |
|
|
71
|
+
| `comment` | Generate PR comment from audit results |
|
|
72
|
+
| `fix <job-id>` | Generate a fix patch for an issue |
|
|
73
|
+
| `fix-all <job-id>` | Generate fix patches for all issues |
|
|
74
|
+
| `verify` | Verify that a patch fixes an issue |
|
|
75
|
+
|
|
76
|
+
### Utility
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---------|-------------|
|
|
80
|
+
| `doctor` | Diagnose CLI health (config, auth, network) |
|
|
81
|
+
| `login` | Authenticate with VertaaUX |
|
|
82
|
+
| `logout` | Clear stored credentials |
|
|
83
|
+
| `whoami` | Show current authentication status |
|
|
84
|
+
| `init` | Create `.vertaaux.yml` configuration |
|
|
85
|
+
| `status <job-id>` | Check audit job status |
|
|
86
|
+
| `upload <file>` | Upload audit results to cloud storage |
|
|
87
|
+
| `download <id>` | Download audit results from cloud storage |
|
|
88
|
+
|
|
89
|
+
### Aliases
|
|
90
|
+
|
|
91
|
+
| Command | Alias For |
|
|
92
|
+
|---------|-----------|
|
|
93
|
+
| `a11y <url>` | Accessibility-focused audit (filters for a11y issues) |
|
|
94
|
+
| `scan <url>` | UX scan (alias for audit) |
|
|
95
|
+
| `compare <urlA> <urlB>` | Compare audits of two URLs |
|
|
96
|
+
|
|
97
|
+
## Output Formats
|
|
98
|
+
|
|
99
|
+
Formats are **per-command**, not global. Each command supports a different set of formats:
|
|
100
|
+
|
|
101
|
+
| Command | Formats | Default |
|
|
102
|
+
|---------|---------|---------|
|
|
103
|
+
| `audit` | `human`, `json`, `sarif`, `junit`, `html` | `human` |
|
|
104
|
+
| `comment` | `json`, `markdown` | `markdown` |
|
|
105
|
+
| `explain` | `human`, `json` | `human` |
|
|
106
|
+
| `policy show` | `json`, `yaml` | `yaml` |
|
|
107
|
+
| `diff` | `human`, `json` | `human` |
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
vertaa audit https://example.com --format json
|
|
113
|
+
vertaa audit https://example.com --format sarif > results.sarif
|
|
114
|
+
vertaa comment --input results.json --format markdown
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Machine-Readable Output
|
|
118
|
+
|
|
119
|
+
The `--machine` global flag enables strict machine-readable mode:
|
|
120
|
+
|
|
121
|
+
- **stdout**: JSON data only (no banners, no diagnostics)
|
|
122
|
+
- **stderr**: All diagnostic and progress output
|
|
123
|
+
- JSON output is wrapped in an envelope:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"meta": {
|
|
128
|
+
"version": "0.1.0",
|
|
129
|
+
"timestamp": "2026-02-08T12:00:00.000Z",
|
|
130
|
+
"command": "audit",
|
|
131
|
+
"args": ["https://example.com", "--format", "json"]
|
|
132
|
+
},
|
|
133
|
+
"data": {
|
|
134
|
+
"scores": { "overall": 85 },
|
|
135
|
+
"issues": []
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Piping
|
|
141
|
+
|
|
142
|
+
All diagnostic output goes to stderr, keeping stdout clean for piping:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
vertaa audit https://example.com --format json | jq '.data.scores'
|
|
146
|
+
vertaa audit https://example.com --format json > results.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Global Options
|
|
150
|
+
|
|
151
|
+
These options work with any command:
|
|
152
|
+
|
|
153
|
+
| Option | Description |
|
|
154
|
+
|--------|-------------|
|
|
155
|
+
| `-b, --base <url>` | API base URL override |
|
|
156
|
+
| `-c, --config <path>` | Explicit config file path |
|
|
157
|
+
| `-q, --quiet` | Suppress banner and non-essential output |
|
|
158
|
+
| `--no-banner` | Hide the V-mark banner |
|
|
159
|
+
| `--machine` | Strict machine-readable output mode |
|
|
160
|
+
| `-v, --version` | Show version number |
|
|
161
|
+
| `-h, --help` | Show help for command |
|
|
162
|
+
|
|
163
|
+
## Exit Codes
|
|
164
|
+
|
|
165
|
+
| Code | Meaning | When |
|
|
166
|
+
|------|---------|------|
|
|
167
|
+
| `0` | Success | Audit passed, no issues above threshold |
|
|
168
|
+
| `1` | Issues found | Issues at or above `--fail-on` severity |
|
|
169
|
+
| `2` | Error | Invalid input, validation errors, network failures |
|
|
170
|
+
| `3` | Threshold breach | Score below `--threshold` value |
|
|
171
|
+
|
|
172
|
+
Exit code `2` is used for all validation errors, including:
|
|
173
|
+
- Invalid flag values (`--timeout abc`)
|
|
174
|
+
- Unknown enum values (`--mode bogus`)
|
|
175
|
+
- Missing required arguments
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
The CLI uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig) for configuration file auto-detection.
|
|
180
|
+
|
|
181
|
+
### Config File Search Order
|
|
182
|
+
|
|
183
|
+
1. `.vertaaux.yml`
|
|
184
|
+
2. `.vertaaux.yaml`
|
|
185
|
+
3. `.vertaaux.json`
|
|
186
|
+
4. `vertaaux.config.js`
|
|
187
|
+
5. `vertaaux.config.mjs`
|
|
188
|
+
6. `vertaaux.config.cjs`
|
|
189
|
+
7. `package.json` (`vertaaux` key)
|
|
190
|
+
|
|
191
|
+
Or specify explicitly:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
vertaa audit https://example.com --config path/to/.vertaaux.yml
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Configuration Precedence
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
flag > env var > config file > default
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Key Config Fields
|
|
204
|
+
|
|
205
|
+
From `VertaauxConfig` interface in `src/config/schema.ts`:
|
|
206
|
+
|
|
207
|
+
| Field | Type | Default | Description |
|
|
208
|
+
|-------|------|---------|-------------|
|
|
209
|
+
| `mode` | `basic\|standard\|deep` | `basic` | Audit depth |
|
|
210
|
+
| `threshold` | `number` | `0` | Minimum passing score (0-100) |
|
|
211
|
+
| `failOn` | `error\|warning\|info` | - | Fail on severity |
|
|
212
|
+
| `output.format` | `auto\|json\|sarif\|...` | `auto` | Output format |
|
|
213
|
+
| `output.groupBy` | `severity\|category\|route` | `severity` | Issue grouping |
|
|
214
|
+
| `baseline.path` | `string` | `.vertaaux/baseline.json` | Baseline file path |
|
|
215
|
+
| `baseline.autoUpdate` | `boolean` | `false` | Auto-update baseline |
|
|
216
|
+
| `ci.template` | `github\|gitlab\|...` | `none` | CI template |
|
|
217
|
+
| `timeout` | `number` | `60000` | Audit timeout (ms) |
|
|
218
|
+
| `interval` | `number` | `5000` | Poll interval (ms) |
|
|
219
|
+
|
|
220
|
+
### Example Configuration
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
# .vertaaux.yml
|
|
224
|
+
$schema: https://vertaaux.ai/schemas/config.json
|
|
225
|
+
|
|
226
|
+
mode: standard
|
|
227
|
+
threshold: 80
|
|
228
|
+
failOn: error
|
|
229
|
+
|
|
230
|
+
output:
|
|
231
|
+
format: auto
|
|
232
|
+
groupBy: severity
|
|
233
|
+
|
|
234
|
+
baseline:
|
|
235
|
+
path: .vertaaux/baseline.json
|
|
236
|
+
autoUpdate: false
|
|
237
|
+
|
|
238
|
+
ci:
|
|
239
|
+
template: github
|
|
240
|
+
|
|
241
|
+
timeout: 60000
|
|
242
|
+
interval: 5000
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Create a starter configuration:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
vertaa init
|
|
249
|
+
vertaa init --ci github --yes
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Security
|
|
253
|
+
|
|
254
|
+
### Branch Name Validation
|
|
255
|
+
|
|
256
|
+
Branch names passed via `--base-branch` and `--branch` flags are validated against an allowlist regex:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
/^[a-zA-Z0-9._\/-]+$/
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- Maximum length: 255 characters
|
|
263
|
+
- Shell metacharacters (`;`, `|`, `$`, `` ` ``, etc.) are rejected
|
|
264
|
+
- Standard git branch names work as expected: `main`, `feature/login`, `release/v1.2.3`
|
|
265
|
+
|
|
266
|
+
### Artifact Path Protection
|
|
267
|
+
|
|
268
|
+
Downloaded artifact filenames are validated to stay within the output directory:
|
|
269
|
+
|
|
270
|
+
- Path traversal attempts (`../`) are rejected with an error
|
|
271
|
+
- All artifact paths are resolved and checked against the target directory boundary
|
|
272
|
+
- This prevents writing to arbitrary filesystem locations
|
|
273
|
+
|
|
274
|
+
### Credential Filtering
|
|
275
|
+
|
|
276
|
+
JSON envelope output automatically filters CLI arguments containing API keys or Bearer tokens from the `args` metadata field.
|
|
277
|
+
|
|
278
|
+
## Environment Variables
|
|
279
|
+
|
|
280
|
+
| Variable | Purpose |
|
|
281
|
+
|----------|---------|
|
|
282
|
+
| `VERTAAUX_API_KEY` | API authentication key |
|
|
283
|
+
| `VERTAAUX_TOKEN` | Alternative auth token (checked first) |
|
|
284
|
+
| `VERTAAUX_API_BASE` | API base URL override |
|
|
285
|
+
| `NO_COLOR` | Disable colored output |
|
|
286
|
+
| `FORCE_COLOR` | Force colored output |
|
|
287
|
+
|
|
288
|
+
## CI/CD Integration
|
|
289
|
+
|
|
290
|
+
### GitHub Actions
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
- name: Run audit
|
|
294
|
+
env:
|
|
295
|
+
VERTAAUX_API_KEY: ${{ secrets.VERTAAUX_API_KEY }}
|
|
296
|
+
run: |
|
|
297
|
+
npx @vertaaux/cli audit https://example.com \
|
|
298
|
+
--format sarif \
|
|
299
|
+
--fail-on error \
|
|
300
|
+
--threshold 80
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Exit Code Gating
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# Fail CI if score below 80
|
|
307
|
+
vertaa audit https://example.com --threshold 80
|
|
308
|
+
|
|
309
|
+
# Fail CI if any error-severity issues found
|
|
310
|
+
vertaa audit https://example.com --fail-on error
|
|
311
|
+
|
|
312
|
+
# Both
|
|
313
|
+
vertaa audit https://example.com --threshold 80 --fail-on error
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Error Messages
|
|
317
|
+
|
|
318
|
+
The CLI provides branded error messages with contextual help:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
vertaa error: expected a number, got "abc"
|
|
322
|
+
──────────────────────────────────
|
|
323
|
+
│ flag: --timeout
|
|
324
|
+
│ value: abc
|
|
325
|
+
│
|
|
326
|
+
│ hint: Run vertaa <command> --help for all options
|
|
327
|
+
──────────────────────────────────
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For enum values, typo suggestions are provided:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
vertaa error: invalid value for --mode
|
|
334
|
+
│ flag: --mode
|
|
335
|
+
│ value: depp
|
|
336
|
+
│
|
|
337
|
+
│ hint: Did you mean "deep"?
|
|
338
|
+
│ valid: basic, standard, deep
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Related
|
|
342
|
+
|
|
343
|
+
- [Migration Guide](./MIGRATION.md) -- Breaking changes in this release
|
|
344
|
+
- [Configuration Docs](https://vertaaux.ai/docs/cli/configuration) -- Full config reference
|
|
345
|
+
- [CI/CD Integration](https://vertaaux.ai/docs/cli/cicd) -- Pipeline setup guides
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI token handling for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* CI tokens are long-lived API keys used in CI/CD pipelines.
|
|
5
|
+
* They can be passed via environment variables or --token flag.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get CI token from environment variables.
|
|
9
|
+
*
|
|
10
|
+
* Checks VERTAAUX_TOKEN first, then VERTAAUX_API_KEY.
|
|
11
|
+
*
|
|
12
|
+
* @returns Token string or null if not found
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCIToken(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* API response for token validation.
|
|
17
|
+
*/
|
|
18
|
+
interface TokenValidationResponse {
|
|
19
|
+
/** Whether token is valid */
|
|
20
|
+
valid: boolean;
|
|
21
|
+
/** User or organization ID */
|
|
22
|
+
user_id?: string;
|
|
23
|
+
/** Organization name */
|
|
24
|
+
organization?: string;
|
|
25
|
+
/** Scopes granted to token */
|
|
26
|
+
scopes?: string[];
|
|
27
|
+
/** Token expiration (if any) */
|
|
28
|
+
expires_at?: string;
|
|
29
|
+
/** Error message if invalid */
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate a CI token against the API.
|
|
34
|
+
*
|
|
35
|
+
* @param token - Token to validate
|
|
36
|
+
* @param apiBase - API base URL (optional)
|
|
37
|
+
* @returns true if token is valid
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateCIToken(token: string, apiBase?: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Get token info from the API.
|
|
42
|
+
*
|
|
43
|
+
* @param token - Token to check
|
|
44
|
+
* @param apiBase - API base URL (optional)
|
|
45
|
+
* @returns Token info or null if invalid/error
|
|
46
|
+
*/
|
|
47
|
+
export declare function getTokenInfo(token: string, apiBase?: string): Promise<TokenValidationResponse | null>;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=ci-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-token.d.ts","sourceRoot":"","sources":["../../src/auth/ci-token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAQ1C;AAED;;GAEG;AACH,UAAU,uBAAuB;IAC/B,6BAA6B;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAkBzC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI token handling for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* CI tokens are long-lived API keys used in CI/CD pipelines.
|
|
5
|
+
* They can be passed via environment variables or --token flag.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Environment variable names for CI tokens.
|
|
9
|
+
* Checked in order of preference.
|
|
10
|
+
*/
|
|
11
|
+
const TOKEN_ENV_VARS = ["VERTAAUX_TOKEN", "VERTAAUX_API_KEY"];
|
|
12
|
+
/**
|
|
13
|
+
* Get CI token from environment variables.
|
|
14
|
+
*
|
|
15
|
+
* Checks VERTAAUX_TOKEN first, then VERTAAUX_API_KEY.
|
|
16
|
+
*
|
|
17
|
+
* @returns Token string or null if not found
|
|
18
|
+
*/
|
|
19
|
+
export function getCIToken() {
|
|
20
|
+
for (const envVar of TOKEN_ENV_VARS) {
|
|
21
|
+
const value = process.env[envVar];
|
|
22
|
+
if (value) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Default API base URL.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_API_BASE = "https://vertaaux.ai/v1";
|
|
32
|
+
/**
|
|
33
|
+
* Validate a CI token against the API.
|
|
34
|
+
*
|
|
35
|
+
* @param token - Token to validate
|
|
36
|
+
* @param apiBase - API base URL (optional)
|
|
37
|
+
* @returns true if token is valid
|
|
38
|
+
*/
|
|
39
|
+
export async function validateCIToken(token, apiBase = DEFAULT_API_BASE) {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${apiBase}/auth/validate`, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
headers: {
|
|
44
|
+
"X-API-Key": token,
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const data = (await response.json());
|
|
52
|
+
return data.valid === true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Network errors mean we can't validate, treat as invalid
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get token info from the API.
|
|
61
|
+
*
|
|
62
|
+
* @param token - Token to check
|
|
63
|
+
* @param apiBase - API base URL (optional)
|
|
64
|
+
* @returns Token info or null if invalid/error
|
|
65
|
+
*/
|
|
66
|
+
export async function getTokenInfo(token, apiBase = DEFAULT_API_BASE) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(`${apiBase}/auth/validate`, {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers: {
|
|
71
|
+
"X-API-Key": token,
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return response.json();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Device Code Flow implementation (RFC 8628).
|
|
3
|
+
*
|
|
4
|
+
* Used for interactive authentication where the user authenticates
|
|
5
|
+
* in a browser and the CLI polls for completion.
|
|
6
|
+
*
|
|
7
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8628
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Device code response from authorization server.
|
|
11
|
+
*/
|
|
12
|
+
export interface DeviceCodeResponse {
|
|
13
|
+
/** Device verification code */
|
|
14
|
+
device_code: string;
|
|
15
|
+
/** User code to display */
|
|
16
|
+
user_code: string;
|
|
17
|
+
/** Verification URL for user to visit */
|
|
18
|
+
verification_uri: string;
|
|
19
|
+
/** Optional complete URL with user_code embedded */
|
|
20
|
+
verification_uri_complete?: string;
|
|
21
|
+
/** Recommended polling interval in seconds */
|
|
22
|
+
interval: number;
|
|
23
|
+
/** Code expiration in seconds */
|
|
24
|
+
expires_in: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Token response from authorization server.
|
|
28
|
+
*/
|
|
29
|
+
export interface TokenResponse {
|
|
30
|
+
/** Access token */
|
|
31
|
+
access_token: string;
|
|
32
|
+
/** Token type (usually "Bearer") */
|
|
33
|
+
token_type: string;
|
|
34
|
+
/** Token lifetime in seconds */
|
|
35
|
+
expires_in: number;
|
|
36
|
+
/** Refresh token for getting new access tokens */
|
|
37
|
+
refresh_token?: string;
|
|
38
|
+
/** Scope granted */
|
|
39
|
+
scope?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Device flow result returned to caller.
|
|
43
|
+
*/
|
|
44
|
+
export interface DeviceFlowResult {
|
|
45
|
+
/** Access token for API calls */
|
|
46
|
+
accessToken: string;
|
|
47
|
+
/** Refresh token for token renewal */
|
|
48
|
+
refreshToken?: string;
|
|
49
|
+
/** Token expiration in seconds from now */
|
|
50
|
+
expiresIn: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the OAuth Device Code Flow.
|
|
54
|
+
*
|
|
55
|
+
* 1. Request device code from authorization server
|
|
56
|
+
* 2. Display user code and verification URL
|
|
57
|
+
* 3. Poll for token with countdown
|
|
58
|
+
* 4. Return tokens on success
|
|
59
|
+
*
|
|
60
|
+
* @param clientId - OAuth client ID for the CLI
|
|
61
|
+
* @param authBase - Base URL for auth endpoints (default: https://vertaaux.ai)
|
|
62
|
+
* @returns Device flow result with tokens
|
|
63
|
+
* @throws Error if authorization fails or times out
|
|
64
|
+
*/
|
|
65
|
+
export declare function startDeviceFlow(clientId: string, authBase?: string): Promise<DeviceFlowResult>;
|
|
66
|
+
//# sourceMappingURL=device-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Device Code Flow implementation (RFC 8628).
|
|
3
|
+
*
|
|
4
|
+
* Used for interactive authentication where the user authenticates
|
|
5
|
+
* in a browser and the CLI polls for completion.
|
|
6
|
+
*
|
|
7
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8628
|
|
8
|
+
*/
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
/**
|
|
11
|
+
* Format remaining time as MM:SS.
|
|
12
|
+
*/
|
|
13
|
+
function formatRemaining(seconds) {
|
|
14
|
+
const mins = Math.floor(seconds / 60);
|
|
15
|
+
const secs = seconds % 60;
|
|
16
|
+
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default API base URL for VertaaUX.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_AUTH_BASE = "https://vertaaux.ai";
|
|
22
|
+
/**
|
|
23
|
+
* Default timeout for device flow (5 minutes).
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
26
|
+
/**
|
|
27
|
+
* Start the OAuth Device Code Flow.
|
|
28
|
+
*
|
|
29
|
+
* 1. Request device code from authorization server
|
|
30
|
+
* 2. Display user code and verification URL
|
|
31
|
+
* 3. Poll for token with countdown
|
|
32
|
+
* 4. Return tokens on success
|
|
33
|
+
*
|
|
34
|
+
* @param clientId - OAuth client ID for the CLI
|
|
35
|
+
* @param authBase - Base URL for auth endpoints (default: https://vertaaux.ai)
|
|
36
|
+
* @returns Device flow result with tokens
|
|
37
|
+
* @throws Error if authorization fails or times out
|
|
38
|
+
*/
|
|
39
|
+
export async function startDeviceFlow(clientId, authBase = DEFAULT_AUTH_BASE) {
|
|
40
|
+
// Step 1: Request device code
|
|
41
|
+
const deviceCodeResponse = await requestDeviceCode(clientId, authBase);
|
|
42
|
+
// Step 2: Display instructions
|
|
43
|
+
console.log("\n");
|
|
44
|
+
console.log(" To authenticate, visit:");
|
|
45
|
+
console.log(` ${deviceCodeResponse.verification_uri}`);
|
|
46
|
+
console.log("\n");
|
|
47
|
+
console.log(` Enter code: ${deviceCodeResponse.user_code}`);
|
|
48
|
+
console.log("\n");
|
|
49
|
+
if (deviceCodeResponse.verification_uri_complete) {
|
|
50
|
+
console.log(` Or open: ${deviceCodeResponse.verification_uri_complete}`);
|
|
51
|
+
console.log("\n");
|
|
52
|
+
}
|
|
53
|
+
// Step 3: Poll for token with countdown
|
|
54
|
+
const tokens = await pollForToken(clientId, deviceCodeResponse.device_code, deviceCodeResponse.interval, deviceCodeResponse.expires_in, authBase);
|
|
55
|
+
return tokens;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Request a device code from the authorization server.
|
|
59
|
+
*/
|
|
60
|
+
async function requestDeviceCode(clientId, authBase) {
|
|
61
|
+
const url = `${authBase}/oauth/device/code`;
|
|
62
|
+
const response = await fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
66
|
+
},
|
|
67
|
+
body: new URLSearchParams({
|
|
68
|
+
client_id: clientId,
|
|
69
|
+
scope: "audit:read audit:write baseline:read baseline:write",
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const text = await response.text();
|
|
74
|
+
throw new Error(`Failed to request device code: ${response.status} ${text}`);
|
|
75
|
+
}
|
|
76
|
+
return response.json();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Poll the token endpoint until authorization completes.
|
|
80
|
+
*/
|
|
81
|
+
async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeconds, authBase) {
|
|
82
|
+
const url = `${authBase}/oauth/token`;
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
const timeoutMs = Math.min(expiresInSeconds, DEFAULT_TIMEOUT_SECONDS) * 1000;
|
|
85
|
+
let interval = intervalSeconds * 1000; // Convert to milliseconds
|
|
86
|
+
// Start spinner with countdown
|
|
87
|
+
const spinner = ora({
|
|
88
|
+
text: `Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`,
|
|
89
|
+
stream: process.stderr,
|
|
90
|
+
}).start();
|
|
91
|
+
try {
|
|
92
|
+
while (true) {
|
|
93
|
+
// Check timeout
|
|
94
|
+
const elapsed = Date.now() - startTime;
|
|
95
|
+
const remaining = Math.max(0, timeoutMs - elapsed);
|
|
96
|
+
if (remaining === 0) {
|
|
97
|
+
throw new Error("Authorization timed out. Please try again.");
|
|
98
|
+
}
|
|
99
|
+
// Update spinner with countdown
|
|
100
|
+
spinner.text = `Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`;
|
|
101
|
+
// Wait for poll interval
|
|
102
|
+
await sleep(interval);
|
|
103
|
+
// Poll token endpoint
|
|
104
|
+
const response = await fetch(url, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
108
|
+
},
|
|
109
|
+
body: new URLSearchParams({
|
|
110
|
+
client_id: clientId,
|
|
111
|
+
device_code: deviceCode,
|
|
112
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
// Success
|
|
116
|
+
if (response.ok) {
|
|
117
|
+
const tokens = (await response.json());
|
|
118
|
+
spinner.succeed("Authorization successful!");
|
|
119
|
+
return {
|
|
120
|
+
accessToken: tokens.access_token,
|
|
121
|
+
refreshToken: tokens.refresh_token,
|
|
122
|
+
expiresIn: tokens.expires_in,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Handle error responses
|
|
126
|
+
const error = (await response.json());
|
|
127
|
+
switch (error.error) {
|
|
128
|
+
case "authorization_pending":
|
|
129
|
+
// User hasn't completed authorization yet, continue polling
|
|
130
|
+
continue;
|
|
131
|
+
case "slow_down":
|
|
132
|
+
// Server requests slower polling
|
|
133
|
+
interval += 5000; // Add 5 seconds
|
|
134
|
+
continue;
|
|
135
|
+
case "access_denied":
|
|
136
|
+
throw new Error("Authorization denied by user.");
|
|
137
|
+
case "expired_token":
|
|
138
|
+
throw new Error("Device code expired. Please try again.");
|
|
139
|
+
default:
|
|
140
|
+
throw new Error(error.error_description || `Authorization failed: ${error.error}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
// Ensure spinner is stopped
|
|
146
|
+
if (spinner.isSpinning) {
|
|
147
|
+
spinner.stop();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Sleep for specified milliseconds.
|
|
153
|
+
*/
|
|
154
|
+
function sleep(ms) {
|
|
155
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
156
|
+
}
|