@coldtea/qa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/bin/coldtea-qa.mjs +114 -0
- package/dist/main.js +4760 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Coldtea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# @coldtea/qa — the `coldtea-qa` CLI
|
|
2
|
+
|
|
3
|
+
Run Coldtea QA from a terminal or CI: manage projects, tests and groups,
|
|
4
|
+
start runs and follow them to a verdict, upload mobile builds, merge
|
|
5
|
+
projects. Every command talks to the public `/v1` API and nothing else.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The CLI is TypeScript with **zero runtime dependencies**, compiled to
|
|
10
|
+
`dist/` before it runs. The build is not optional: Node refuses to strip
|
|
11
|
+
types from anything under `node_modules`, so an installed package has to
|
|
12
|
+
contain JavaScript. `npm install` builds it for you via the package's
|
|
13
|
+
`prepare` script; `npm run build -w @coldtea/qa` rebuilds it by hand.
|
|
14
|
+
Compiled output needs Node >= 20.6.
|
|
15
|
+
|
|
16
|
+
One consequence worth knowing: `prepare` runs on every root `npm install` and
|
|
17
|
+
`npm ci`, so a type error in this package fails INSTALL — before the dedicated
|
|
18
|
+
typecheck job can report it with a useful message. That is the price of the
|
|
19
|
+
binary always being built, and it is the right trade, but a confusing `npm ci`
|
|
20
|
+
failure in an unrelated CI job is usually this.
|
|
21
|
+
|
|
22
|
+
Inside this monorepo:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
node packages/qa-cli/bin/coldtea-qa.mjs --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The API reference
|
|
29
|
+
|
|
30
|
+
The CLI talks to `/v1` and nothing else, so the API reference IS the CLI's
|
|
31
|
+
reference for what any command can do:
|
|
32
|
+
|
|
33
|
+
https://www.coldtea.ai/openapi/coldtea-v1.json
|
|
34
|
+
|
|
35
|
+
and the same thing as a page you can read:
|
|
36
|
+
|
|
37
|
+
https://www.coldtea.ai/a/art_lsopxy23iqrvtojwpdiaole7w7x1325skjbjpoms19sygt6yc9
|
|
38
|
+
|
|
39
|
+
OpenAPI 3.1, generated from the routes themselves — every endpoint, the scope
|
|
40
|
+
it needs, its request body and what it returns. Import it into Postman,
|
|
41
|
+
Insomnia, Apidog, or hand it to an agent. It is regenerated on every change and
|
|
42
|
+
CI fails when it drifts, so it cannot describe an API we no longer have.
|
|
43
|
+
|
|
44
|
+
## Auth
|
|
45
|
+
|
|
46
|
+
Create an API key in the Coldtea dashboard, then:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
export COLDTEA_API_KEY=coldtea_sk_…
|
|
50
|
+
coldtea-qa whoami
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The key identifies your organization; there is no organization flag. Most
|
|
54
|
+
commands work on resource ids alone. Commands that list or create inside a
|
|
55
|
+
project take `--project <id>` or `COLDTEA_PROJECT_ID`.
|
|
56
|
+
|
|
57
|
+
`COLDTEA_BASE_URL` chooses the server. **Unset it points at production**
|
|
58
|
+
(`https://www.coldtea.ai`); set it to a full origin to point somewhere else.
|
|
59
|
+
Setting it to an EMPTY value is refused rather than falling back to
|
|
60
|
+
production, because that fallback is silent and the key in your hand may not
|
|
61
|
+
be one production should see.
|
|
62
|
+
|
|
63
|
+
**The origin must be https** — every request sends your API key there, and
|
|
64
|
+
plain http would put it on the wire in plaintext. Loopback is the exception
|
|
65
|
+
(`localhost`, `127.0.0.0/8`, `[::1]`), because that is where dev servers
|
|
66
|
+
live. For an http-only private host (`host.docker.internal`, a VPC staging
|
|
67
|
+
mirror), set `COLDTEA_ALLOW_INSECURE_HTTP=1` — an explicit, greppable line
|
|
68
|
+
in your CI file saying the key travels unencrypted.
|
|
69
|
+
|
|
70
|
+
`--api-key` overrides the environment, but prefer the variable: a flag is
|
|
71
|
+
visible in `ps` while the process runs, in shell history, and in any CI log
|
|
72
|
+
that echoes the step it is about to run.
|
|
73
|
+
|
|
74
|
+
## Output, exit codes, CI
|
|
75
|
+
|
|
76
|
+
- `--json` prints one machine-readable JSON document on stdout, in the
|
|
77
|
+
API's own envelope: **`{ data, meta }`, and that is the contract** — the
|
|
78
|
+
same shape `/v1` sends and the same shape this CLI's errors use, so one
|
|
79
|
+
reader handles all three. It is deliberately not a CLI-shaped envelope;
|
|
80
|
+
changing it breaks every consumer rather than tidying anything. `meta` carries `requestId` on every
|
|
81
|
+
call, `nextCursor` on a list, and anything else the endpoint sends — so
|
|
82
|
+
`credentials list --json` can see `meta.unlistedCredentials`, and a
|
|
83
|
+
SUCCESSFUL call has a request id to quote, not just a failing one.
|
|
84
|
+
- Errors go to stderr, as `{ "error": …, "meta": { "requestId": … } }` —
|
|
85
|
+
the same envelope the API sends, so one reader handles both. Quote the
|
|
86
|
+
request id in support. **On failure stdout is empty**: check the exit code
|
|
87
|
+
before parsing, because empty is not valid JSON.
|
|
88
|
+
- Color only on a TTY, and never when `NO_COLOR` is set. Nothing prompts
|
|
89
|
+
without a TTY.
|
|
90
|
+
- Exit codes are a contract, and the line between `2` and `4` is the one
|
|
91
|
+
worth knowing: **`2` is worth retrying unchanged, `4` never is.**
|
|
92
|
+
`0` pass (warnings pass unless `--fail-on-warning`) · `1` fail — the app
|
|
93
|
+
is broken; the only exit that means this · `2` couldn't run or verify:
|
|
94
|
+
a timeout, a batch with skipped tests, a 5xx, rate limiting, no credits,
|
|
95
|
+
a dropped connection · `3` conflict needing a human (the 409 family) ·
|
|
96
|
+
`4` auth or configuration error, **including a request the API refused as
|
|
97
|
+
malformed or as naming something that is not there** — a typo'd test id
|
|
98
|
+
is `4`, not `2`, because retrying it can only fail again.
|
|
99
|
+
- A group run that queued nothing exits `2`, never `0`. Nothing was
|
|
100
|
+
verified, so nothing passed — and a batch replayed from an earlier run of
|
|
101
|
+
the same group on the same commit says so rather than looking fresh.
|
|
102
|
+
|
|
103
|
+
## Projects
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
coldtea-qa projects list [--include-archived]
|
|
107
|
+
coldtea-qa projects create "Storefront"
|
|
108
|
+
coldtea-qa projects get prj_…
|
|
109
|
+
coldtea-qa projects link-repo prj_… --provider github --repository-id 812345
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`--repository-id` is the provider's stable id, not a URL or `owner/name`.
|
|
113
|
+
A project holds at most one repository: linking over an existing link
|
|
114
|
+
exits 3 and names the current repo. If the repo already holds report-name
|
|
115
|
+
reservations that clash with this project's rule names, the CLI lists
|
|
116
|
+
every conflict.
|
|
117
|
+
|
|
118
|
+
## Tests and groups
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
coldtea-qa tests list --project prj_… # or COLDTEA_PROJECT_ID
|
|
122
|
+
coldtea-qa tests create "Checkout with a saved card" --url https://shop.example
|
|
123
|
+
coldtea-qa tests create "Smoke the preview" --deployment
|
|
124
|
+
coldtea-qa tests create "Login on device" --mobile ios
|
|
125
|
+
coldtea-qa tests get mqt_…
|
|
126
|
+
coldtea-qa tests update mqt_… --group mqg_… # move between groups
|
|
127
|
+
coldtea-qa tests delete mqt_… # archives; cancels runs
|
|
128
|
+
|
|
129
|
+
coldtea-qa groups list
|
|
130
|
+
coldtea-qa groups create "Smoke" --description "Fast checks"
|
|
131
|
+
coldtea-qa groups update mqg_… --name "Nightly"
|
|
132
|
+
coldtea-qa groups delete mqg_… # archives its tests too
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A test targets exactly one of: a fixed URL (`--url`), the linked
|
|
136
|
+
repository's deployments (`--deployment` — link a repo first), or a mobile
|
|
137
|
+
app (`--mobile android|ios`). Deletes are soft (archive), and deployment
|
|
138
|
+
tests cannot be moved between groups.
|
|
139
|
+
|
|
140
|
+
## Environments, credentials and rules
|
|
141
|
+
|
|
142
|
+
Enough to set a project up from nothing, which is what an agent or a new
|
|
143
|
+
teammate actually needs.
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
coldtea-qa repositories list # ids for link-repo
|
|
147
|
+
coldtea-qa environments create "Staging" --url https://staging.example
|
|
148
|
+
coldtea-qa rules create "PRs" --deployment --branch main --groups mqg_… --checks
|
|
149
|
+
coldtea-qa skips list # why a rule did not run
|
|
150
|
+
coldtea-qa apps list # mobile apps and their builds
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
An **environment** is the place a run opens in. `run test --environment env_…`
|
|
154
|
+
has always named one; this is how you make one. Only web environments are
|
|
155
|
+
creatable here — a mobile one needs a device profile from the provider.
|
|
156
|
+
|
|
157
|
+
A **credential** is a secret an environment signs in with, and the secret is
|
|
158
|
+
read from stdin, never a flag: a command line is visible in shell history, in
|
|
159
|
+
`ps`, and in CI logs.
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
echo -n "$VERCEL_BYPASS" | coldtea-qa credentials create "Preview wall" \
|
|
163
|
+
--method vercel_bypass
|
|
164
|
+
coldtea-qa environments create "Preview" --url https://preview.example \
|
|
165
|
+
--credential qcr_… --vercel-project prj_…
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Attaching a stored credential needs a **scope**, and it is not a formality:
|
|
169
|
+
the address on the environment does not bound the secret, because a run brings
|
|
170
|
+
its own URL. `--vercel-project <id>` spends it only on that Vercel project's
|
|
171
|
+
deployments; `--any-url` spends it on every URL a run brings, which is
|
|
172
|
+
unbounded and has to be said out loud rather than by omission.
|
|
173
|
+
|
|
174
|
+
Nothing reads a secret back. `credentials list` is metadata only, and it may
|
|
175
|
+
say that credentials exist it cannot name — those were stored before
|
|
176
|
+
credentials recorded a TeaHouse.
|
|
177
|
+
|
|
178
|
+
A **rule** is what runs QA when nobody asks, and it is the only thing that
|
|
179
|
+
reports a check onto a pull request — starting a run directly stamps the commit
|
|
180
|
+
and posts nothing, because check names are reserved per repository by the rule
|
|
181
|
+
that owns them.
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
coldtea-qa rules create "CI" --webhook --groups mqg_… --checks
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A webhook rule's hook URL is printed **once**, on create, because it contains
|
|
188
|
+
the token. Put it in your CI's secrets then; there is no way to read it back.
|
|
189
|
+
|
|
190
|
+
## Runs
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
coldtea-qa run test mqt_… --wait --timeout 10m
|
|
194
|
+
coldtea-qa run group mqg_… --wait
|
|
195
|
+
coldtea-qa runs list --project prj_… --status completed --outcome failed --since 2026-08-01
|
|
196
|
+
|
|
197
|
+
coldtea-qa run test mqt_… --wait --environment env_… # run in one place
|
|
198
|
+
coldtea-qa run test mqt_… --wait --environment none # run signed out
|
|
199
|
+
coldtea-qa run group mqg_… --wait \
|
|
200
|
+
--vcs-provider github --vcs-repository-id 812345 \
|
|
201
|
+
--vcs-head-sha "$BITRISE_GIT_COMMIT" --vcs-pr-number "$BITRISE_PULL_REQUEST"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Without `--wait`, a run start exits 0 as soon as the queue accepts and
|
|
205
|
+
prints the run id(s) and report URL(s). With `--wait`, the CLI polls
|
|
206
|
+
until a verdict and the exit code reflects it; on timeout it exits 2 and
|
|
207
|
+
the run keeps going server-side. A link is printed on every path — pass,
|
|
208
|
+
fail, and timeout. It is the run's report page, which outlives the run;
|
|
209
|
+
the live view URL is only a fallback, because a finished run has no live
|
|
210
|
+
browser session left to look at.
|
|
211
|
+
|
|
212
|
+
`--environment` names the place a run opens in, and replaces the group's
|
|
213
|
+
environment whole rather than merging with it. `--environment none` runs
|
|
214
|
+
signed out. Omitting it inherits the group's.
|
|
215
|
+
|
|
216
|
+
The `--vcs-*` group attaches a run to a pull request: it stamps the commit
|
|
217
|
+
and PR onto the run so the dashboard and report page show what was tested.
|
|
218
|
+
It does **not** post a check back onto the PR — check names are reserved
|
|
219
|
+
per repository by rules, so a check from CI comes from a webhook rule
|
|
220
|
+
(`POST /v1/hooks/<token>`), not from a run start. All four flags travel
|
|
221
|
+
together, and an empty value counts as absent, so passing an unset CI
|
|
222
|
+
variable is safe.
|
|
223
|
+
|
|
224
|
+
Deployment tests need `--target-url` (which deployment to test); mobile
|
|
225
|
+
tests without a default build need `--build <artifactId>`.
|
|
226
|
+
|
|
227
|
+
A group start is a batch. Tests skipped at admission (not runnable, or not
|
|
228
|
+
enough credits) never become runs, so the CLI prints `skippedCount` /
|
|
229
|
+
`insufficientCredits` from the 202 and a batch with skips exits 2 at best
|
|
230
|
+
— even when every admitted run passes. A real failure still exits 1.
|
|
231
|
+
|
|
232
|
+
Retries are safe: the CLI derives the idempotency key from the test id (or
|
|
233
|
+
group id, for batches) plus the commit sha in CI, so re-running the same
|
|
234
|
+
CI job replays the original run instead of billing twice. Off CI without a
|
|
235
|
+
git sha, each invocation is a fresh run.
|
|
236
|
+
|
|
237
|
+
Two things that sentence does not say, and both bite. The sha comes from CI
|
|
238
|
+
variables _or_ from `git rev-parse HEAD` in the working directory, so the
|
|
239
|
+
replay happens in any git checkout, not only in CI — re-running a test twice
|
|
240
|
+
from your laptop gives you the first run back, and the CLI now says
|
|
241
|
+
"Replayed run …" rather than "Queued". And the key is scoped **per API
|
|
242
|
+
key**: rotating the key starts a fresh idempotency namespace, so the same
|
|
243
|
+
commit can run, and be billed, twice.
|
|
244
|
+
|
|
245
|
+
## Builds
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
coldtea-qa builds upload app-release.apk --platform android \
|
|
249
|
+
--vcs-provider github --vcs-repository-id 812345 \
|
|
250
|
+
--vcs-head-sha <sha> --vcs-pr-number 42
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Three steps under the hood: initiate (a signed upload URL, 10-minute TTL),
|
|
254
|
+
PUT the file to it (streamed, plain fetch), confirm with the file's
|
|
255
|
+
sha256. The CLI then waits for registration by default — `ready` exits 0
|
|
256
|
+
and prints the artifact id to run against; `failed` exits 2 with the
|
|
257
|
+
failure code (e.g. `sha256_mismatch`). `--no-wait` stops after confirm and
|
|
258
|
+
hands back a build id — read it later with `coldtea-qa builds get <id>`.
|
|
259
|
+
Android takes `.apk`; iOS takes a Simulator `.app` in `.zip`/`.tar.gz`;
|
|
260
|
+
max 1 GB.
|
|
261
|
+
|
|
262
|
+
## Merge
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
coldtea-qa merge prj_old --into prj_new [--yes]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Merging is irreversible, so the survivor is a named flag on purpose — no
|
|
269
|
+
positional argument order to get backwards. The CLI always fetches and
|
|
270
|
+
prints the dry-run plan (what moves, what happens to the repo link) before
|
|
271
|
+
asking. Interactively you must type `merge`; in CI pass `--yes`; with
|
|
272
|
+
neither, it exits 4 without calling the merge endpoint. The merge is then
|
|
273
|
+
polled to `completed`/`failed`; a failed merge keeps what already moved
|
|
274
|
+
and re-running the same command resumes it.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// THIS FILE IS A TWIN. packages/qa-cli/bin and packages/tasks-cli/bin are
|
|
3
|
+
// the same bootstrap with the product names swapped, and a test
|
|
4
|
+
// (tasks-cli's bins.test.ts) asserts they stay identical after
|
|
5
|
+
// normalizing those names — a bin-level fix that lands in one and not the
|
|
6
|
+
// other ships broken with nothing red anywhere.
|
|
7
|
+
//
|
|
8
|
+
// Runs the compiled `dist/`, not `src/`. Node refuses to strip types for
|
|
9
|
+
// anything under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING),
|
|
10
|
+
// which is where this package lives after any install — so pointing at the
|
|
11
|
+
// TypeScript worked from a repo checkout and nowhere else. `npm install`
|
|
12
|
+
// builds dist via the package's prepare script.
|
|
13
|
+
//
|
|
14
|
+
// STDOUT AND STDERR FAIL DIFFERENTLY, AND ONLY STDOUT CAN CHANGE THE EXIT
|
|
15
|
+
// CODE.
|
|
16
|
+
//
|
|
17
|
+
// `coldtea-qa runs list | head` closes the pipe while we are still
|
|
18
|
+
// writing. Node turns that into an unhandled EPIPE and a stack trace in the
|
|
19
|
+
// build log. Swallow it on both streams: the reader has stopped caring what
|
|
20
|
+
// we say, and the exit code — which is what CI actually reads — stays
|
|
21
|
+
// whatever the command decided.
|
|
22
|
+
//
|
|
23
|
+
// Any OTHER stdout failure (ENOSPC on a redirected file, EIO) means the
|
|
24
|
+
// caller did not get the output the exit code vouches for, so the exit code
|
|
25
|
+
// becomes 2: we could not deliver. A stderr failure changes NOTHING: stderr
|
|
26
|
+
// carries diagnostics, and flipping a delivered result to 2 — or worse,
|
|
27
|
+
// overwriting a real verdict — because a log volume filled up would make
|
|
28
|
+
// the exit code lie in the other direction.
|
|
29
|
+
//
|
|
30
|
+
// A RETHROW HERE WOULD BE AN UNCAUGHT EXCEPTION, AND NODE THEN PICKS THE
|
|
31
|
+
// EXIT CODE — deterministically 1 in an adversarial full-disk pass, and 1
|
|
32
|
+
// is the one code the contract reserves for a product verdict. A full disk
|
|
33
|
+
// must never counterfeit one.
|
|
34
|
+
//
|
|
35
|
+
// AND THE POST-runCli CHECK CANNOT BE THE ONLY ONE, because `error` events
|
|
36
|
+
// are asynchronous: a large buffered `--json` payload fails on a later tick
|
|
37
|
+
// than that check. So the last word is an exit handler — `process.exitCode`
|
|
38
|
+
// is still settable there — and it also SAYS what happened with a
|
|
39
|
+
// synchronous fd write, because a silent 2 with healthy stderr explains
|
|
40
|
+
// nothing.
|
|
41
|
+
import { writeSync } from "node:fs";
|
|
42
|
+
|
|
43
|
+
let stdoutFailure = null;
|
|
44
|
+
let failureReported = false;
|
|
45
|
+
process.stdout.on("error", (error) => {
|
|
46
|
+
if (error?.code === "EPIPE") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
stdoutFailure ??= error;
|
|
50
|
+
});
|
|
51
|
+
process.stderr.on("error", (error) => {
|
|
52
|
+
// Diagnostics stream: EPIPE or not, a stderr failure never edits the
|
|
53
|
+
// exit code. Swallowing keeps it from becoming an uncaught exception.
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function reportStreamFailure() {
|
|
57
|
+
if (failureReported) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
failureReported = true;
|
|
61
|
+
try {
|
|
62
|
+
writeSync(
|
|
63
|
+
2,
|
|
64
|
+
`coldtea-qa could not write its output: ${stdoutFailure.message}\n`,
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
// The error stream is gone too. Nothing left to say it on.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.on("exit", () => {
|
|
72
|
+
if (stdoutFailure) {
|
|
73
|
+
reportStreamFailure();
|
|
74
|
+
process.exitCode = 2;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Checked before the import, not in its catch: on an old Node the import
|
|
79
|
+
// fails with whatever syntax or missing-export error it hits first, and that
|
|
80
|
+
// stack tells the reader nothing about the actual problem.
|
|
81
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
82
|
+
if (major < 20 || (major === 20 && minor < 6)) {
|
|
83
|
+
console.error(
|
|
84
|
+
`coldtea-qa needs Node >= 20.6 (found ${process.versions.node}).`,
|
|
85
|
+
);
|
|
86
|
+
process.exitCode = 4;
|
|
87
|
+
} else {
|
|
88
|
+
try {
|
|
89
|
+
const { runCli } = await import("../dist/main.js");
|
|
90
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
91
|
+
// A write that already failed means the caller did not get what the
|
|
92
|
+
// exit code claims. Say so now while stderr is definitely writable;
|
|
93
|
+
// the exit handler covers a failure that fires later than this line.
|
|
94
|
+
if (stdoutFailure) {
|
|
95
|
+
reportStreamFailure();
|
|
96
|
+
process.exitCode = 2;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Only a missing dist means "not built". The same code thrown from
|
|
100
|
+
// inside a command is a real bug and must keep its stack.
|
|
101
|
+
const missingDist =
|
|
102
|
+
error?.code === "ERR_MODULE_NOT_FOUND" &&
|
|
103
|
+
String(error?.url ?? error?.message ?? "").includes("dist/main.js");
|
|
104
|
+
if (missingDist) {
|
|
105
|
+
console.error(
|
|
106
|
+
"coldtea-qa is not built. Run `npm run build -w @coldtea/qa`.",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
console.error(error);
|
|
110
|
+
}
|
|
111
|
+
// Exit 4: the CLI could not even configure itself.
|
|
112
|
+
process.exitCode = 4;
|
|
113
|
+
}
|
|
114
|
+
}
|