qualty 0.1.8 → 0.1.9
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/bin/qualty.js +419 -23
- package/package.json +3 -2
package/bin/qualty.js
CHANGED
|
@@ -1,15 +1,108 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { appendFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
5
8
|
import process from "node:process";
|
|
6
9
|
import { runLocalCi } from "./local-runner.js";
|
|
7
10
|
|
|
8
|
-
/**
|
|
11
|
+
/** CLI backend URL is fixed by Qualty distribution. */
|
|
9
12
|
const DEFAULT_QUALTY_API_URL = "https://qualty-api-development.up.railway.app";
|
|
13
|
+
const QUALTY_CONFIG_DIR = join(homedir(), ".config", "qualty");
|
|
14
|
+
const QUALTY_CONFIG_PATH = join(QUALTY_CONFIG_DIR, "config.json");
|
|
15
|
+
const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
|
|
16
|
+
const QUALTY_SHARED_CREDS_PATH = join(QUALTY_SHARED_CREDS_DIR, "credentials");
|
|
17
|
+
const QUALTY_PROJECT_CONFIG_PATH = join(process.cwd(), ".qualty.json");
|
|
10
18
|
|
|
11
|
-
function
|
|
12
|
-
|
|
19
|
+
function readQualtyConfig() {
|
|
20
|
+
if (!existsSync(QUALTY_CONFIG_PATH)) return {};
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(QUALTY_CONFIG_PATH, "utf8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
25
|
+
return parsed;
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeQualtyConfig(config) {
|
|
32
|
+
mkdirSync(QUALTY_CONFIG_DIR, { recursive: true });
|
|
33
|
+
writeFileSync(QUALTY_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
34
|
+
try {
|
|
35
|
+
chmodSync(QUALTY_CONFIG_PATH, 0o600);
|
|
36
|
+
} catch {
|
|
37
|
+
// best effort on non-posix systems
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readSharedCredentials() {
|
|
42
|
+
if (!existsSync(QUALTY_SHARED_CREDS_PATH)) return {};
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(QUALTY_SHARED_CREDS_PATH, "utf8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
47
|
+
return parsed;
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeSharedCredentials(token) {
|
|
54
|
+
mkdirSync(QUALTY_SHARED_CREDS_DIR, { recursive: true });
|
|
55
|
+
writeFileSync(QUALTY_SHARED_CREDS_PATH, `${JSON.stringify({ api_token: token }, null, 2)}\n`, "utf8");
|
|
56
|
+
try {
|
|
57
|
+
chmodSync(QUALTY_SHARED_CREDS_PATH, 0o600);
|
|
58
|
+
} catch {
|
|
59
|
+
// best effort on non-posix systems
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readProjectConfig() {
|
|
64
|
+
if (!existsSync(QUALTY_PROJECT_CONFIG_PATH)) return {};
|
|
65
|
+
try {
|
|
66
|
+
const raw = readFileSync(QUALTY_PROJECT_CONFIG_PATH, "utf8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
69
|
+
return parsed;
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeProjectConfig(updates) {
|
|
76
|
+
const current = readProjectConfig();
|
|
77
|
+
const next = { ...current };
|
|
78
|
+
for (const [k, v] of Object.entries(updates || {})) {
|
|
79
|
+
if (v === undefined || v === null || v === "") delete next[k];
|
|
80
|
+
else next[k] = v;
|
|
81
|
+
}
|
|
82
|
+
writeFileSync(QUALTY_PROJECT_CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveApiUrl(args, config = {}) {
|
|
86
|
+
void args;
|
|
87
|
+
void config;
|
|
88
|
+
return String(DEFAULT_QUALTY_API_URL).replace(/\/$/, "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveToken(args, config = {}) {
|
|
92
|
+
const creds = readSharedCredentials();
|
|
93
|
+
return args.token || process.env.QUALTY_API_TOKEN || creds.api_token || config.api_token || "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveOrgId(args, config = {}) {
|
|
97
|
+
const projectCfg = readProjectConfig();
|
|
98
|
+
return args.org || process.env.QUALTY_ORG_ID || config.org_id || projectCfg.default_org_id || "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function maskSecret(secret) {
|
|
102
|
+
const s = String(secret || "");
|
|
103
|
+
if (!s) return "(empty)";
|
|
104
|
+
if (s.length <= 8) return `${"*".repeat(Math.max(0, s.length - 2))}${s.slice(-2)}`;
|
|
105
|
+
return `${s.slice(0, 4)}...${s.slice(-4)}`;
|
|
13
106
|
}
|
|
14
107
|
|
|
15
108
|
function parseArgs(argv) {
|
|
@@ -39,17 +132,17 @@ function usage() {
|
|
|
39
132
|
console.log(
|
|
40
133
|
[
|
|
41
134
|
"Usage:",
|
|
42
|
-
" qualty
|
|
43
|
-
" qualty
|
|
135
|
+
" qualty setup [--token <bearer-token>] [--org <org-id>]",
|
|
136
|
+
" qualty connect --project <project-id> [--port 3000] [--token <bearer-token>] [--org <org-id>]",
|
|
137
|
+
" qualty run --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--local] [--token <bearer-token>] [--org <org-id>]",
|
|
44
138
|
" [--poll-interval 5] [--timeout 30] [--fail-on-failure true] [--no-view-logs]",
|
|
45
139
|
" [--local] [--device 1440x900] [--headed] [--local-concurrency 4]",
|
|
46
140
|
"",
|
|
47
141
|
" After each run, logs include step results, explanation, and final evaluator output (dashboard \"View logs\" data).",
|
|
48
142
|
" Pass --no-view-logs for a short log only (e.g. very large evaluator text).",
|
|
49
|
-
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--
|
|
143
|
+
" qualty resolve --project <project-id> [--suite-id <suite-id>] [--ids <id1,id2>] [--json] [--token <bearer-token>]",
|
|
50
144
|
"",
|
|
51
145
|
"Env vars:",
|
|
52
|
-
` QUALTY_API_URL Backend API URL (default: ${DEFAULT_QUALTY_API_URL}; set for local/self-hosted)`,
|
|
53
146
|
" QUALTY_API_TOKEN Bearer token used for auth",
|
|
54
147
|
" QUALTY_ORG_ID Org context for scoped API requests",
|
|
55
148
|
" QUALTY_LOCAL_CONCURRENCY Local parallel workers for --local runs (default: 4)",
|
|
@@ -99,11 +192,12 @@ function startCloudflared(token, port) {
|
|
|
99
192
|
}
|
|
100
193
|
|
|
101
194
|
async function runConnect(args) {
|
|
195
|
+
const config = readQualtyConfig();
|
|
102
196
|
const projectId = args.project;
|
|
103
197
|
const port = Number(args.port || 3000);
|
|
104
|
-
const apiUrl = resolveApiUrl(args);
|
|
105
|
-
const token = args
|
|
106
|
-
const orgId = args
|
|
198
|
+
const apiUrl = resolveApiUrl(args, config);
|
|
199
|
+
const token = resolveToken(args, config);
|
|
200
|
+
const orgId = resolveOrgId(args, config);
|
|
107
201
|
|
|
108
202
|
if (!projectId || !token) {
|
|
109
203
|
usage();
|
|
@@ -174,6 +268,163 @@ async function runConnect(args) {
|
|
|
174
268
|
process.on("SIGTERM", shutdown);
|
|
175
269
|
}
|
|
176
270
|
|
|
271
|
+
async function runSetup(args) {
|
|
272
|
+
const existing = readQualtyConfig();
|
|
273
|
+
const sharedCreds = readSharedCredentials();
|
|
274
|
+
const projectCfg = readProjectConfig();
|
|
275
|
+
const rl = createInterface({
|
|
276
|
+
input: process.stdin,
|
|
277
|
+
output: process.stdout,
|
|
278
|
+
});
|
|
279
|
+
const hasStdin = process.stdin.isTTY;
|
|
280
|
+
const ask = async (prompt, fallback = "") => {
|
|
281
|
+
if (!hasStdin) return fallback;
|
|
282
|
+
const answer = await rl.question(prompt);
|
|
283
|
+
const v = String(answer || "").trim();
|
|
284
|
+
return v || fallback;
|
|
285
|
+
};
|
|
286
|
+
const askYesNo = async (prompt, defaultValue = false) => {
|
|
287
|
+
if (!hasStdin) return defaultValue;
|
|
288
|
+
const answer = String(await rl.question(prompt)).trim();
|
|
289
|
+
if (!answer) return defaultValue;
|
|
290
|
+
const raw = answer.toLowerCase();
|
|
291
|
+
if (["y", "yes"].includes(raw)) return true;
|
|
292
|
+
if (["n", "no"].includes(raw)) return false;
|
|
293
|
+
return defaultValue;
|
|
294
|
+
};
|
|
295
|
+
const askChoice = async (prompt, min, max, defaultValue) => {
|
|
296
|
+
if (!hasStdin) return defaultValue;
|
|
297
|
+
while (true) {
|
|
298
|
+
const answer = String(await rl.question(prompt)).trim();
|
|
299
|
+
if (!answer) return defaultValue;
|
|
300
|
+
const n = Number(answer);
|
|
301
|
+
if (Number.isInteger(n) && n >= min && n <= max) return n;
|
|
302
|
+
// eslint-disable-next-line no-console
|
|
303
|
+
console.log(`[qualty] Please enter a number from ${min} to ${max}.`);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const currentOrgId = String(args.org || existing.org_id || projectCfg.default_org_id || "");
|
|
309
|
+
const currentToken = String(args.token || process.env.QUALTY_API_TOKEN || sharedCreds.api_token || existing.api_token || "");
|
|
310
|
+
|
|
311
|
+
let token = "";
|
|
312
|
+
if (args.token) {
|
|
313
|
+
token = String(args.token).trim();
|
|
314
|
+
} else if (currentToken) {
|
|
315
|
+
const updateToken = await askYesNo(`API token already set (${maskSecret(currentToken)}). Update it? [y/N]: `, false);
|
|
316
|
+
if (updateToken) {
|
|
317
|
+
token = String(await ask("New Qualty API token [required]: ", "")).trim();
|
|
318
|
+
} else {
|
|
319
|
+
token = currentToken;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
token = String(await ask("Qualty API token [required]: ", "")).trim();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!token) {
|
|
326
|
+
throw new Error("API token is required. Re-run setup and provide QUALTY_API_TOKEN.");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let organizations = [];
|
|
330
|
+
let currentOrgName = "";
|
|
331
|
+
try {
|
|
332
|
+
const orgResp = await apiRequest({
|
|
333
|
+
apiUrl: resolveApiUrl({}, {}),
|
|
334
|
+
token,
|
|
335
|
+
orgId: "",
|
|
336
|
+
path: "/api/v1/orgs",
|
|
337
|
+
});
|
|
338
|
+
organizations = Array.isArray(orgResp?.organizations) ? orgResp.organizations : [];
|
|
339
|
+
const byId = new Map(
|
|
340
|
+
organizations
|
|
341
|
+
.filter((o) => o && typeof o === "object")
|
|
342
|
+
.map((o) => [String(o.id || ""), String(o.name || "").trim()])
|
|
343
|
+
);
|
|
344
|
+
currentOrgName = byId.get(currentOrgId) || "";
|
|
345
|
+
} catch {
|
|
346
|
+
organizations = [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let orgId = "";
|
|
350
|
+
let orgName = "";
|
|
351
|
+
if (args.org) {
|
|
352
|
+
orgId = String(args.org).trim();
|
|
353
|
+
orgName = currentOrgName || "selected organization";
|
|
354
|
+
} else if (organizations.length > 0) {
|
|
355
|
+
const orgRows = organizations
|
|
356
|
+
.map((o) => ({
|
|
357
|
+
id: String(o?.id || ""),
|
|
358
|
+
name: String(o?.name || "").trim() || "Unnamed org",
|
|
359
|
+
}))
|
|
360
|
+
.filter((o) => o.id);
|
|
361
|
+
const currentIndex = orgRows.findIndex((o) => o.id === currentOrgId);
|
|
362
|
+
if (currentIndex >= 0) {
|
|
363
|
+
const updateOrg = await askYesNo(
|
|
364
|
+
`Organization already set (${orgRows[currentIndex].name}). Update it? [y/N]: `,
|
|
365
|
+
false
|
|
366
|
+
);
|
|
367
|
+
if (!updateOrg) {
|
|
368
|
+
orgId = orgRows[currentIndex].id;
|
|
369
|
+
orgName = orgRows[currentIndex].name;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!orgId) {
|
|
373
|
+
// eslint-disable-next-line no-console
|
|
374
|
+
console.log("[qualty] Choose organization:");
|
|
375
|
+
for (let i = 0; i < orgRows.length; i += 1) {
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.log(` ${i + 1}) ${orgRows[i].name}`);
|
|
378
|
+
}
|
|
379
|
+
const defaultChoice = currentIndex >= 0 ? currentIndex + 1 : 1;
|
|
380
|
+
const picked = await askChoice(
|
|
381
|
+
`Select org [1-${orgRows.length}] (default ${defaultChoice}): `,
|
|
382
|
+
1,
|
|
383
|
+
orgRows.length,
|
|
384
|
+
defaultChoice
|
|
385
|
+
);
|
|
386
|
+
orgId = orgRows[picked - 1].id;
|
|
387
|
+
orgName = orgRows[picked - 1].name;
|
|
388
|
+
}
|
|
389
|
+
} else if (currentOrgId) {
|
|
390
|
+
const updateOrg = await askYesNo("Organization already set. Update it? [y/N]: ", false);
|
|
391
|
+
if (!updateOrg) {
|
|
392
|
+
orgId = currentOrgId;
|
|
393
|
+
orgName = "saved organization";
|
|
394
|
+
} else {
|
|
395
|
+
orgId = String(await ask("New Qualty Org ID [required]: ", "")).trim();
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
orgId = String(await ask("Qualty Org ID [required]: ", "")).trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!orgId) {
|
|
402
|
+
throw new Error("Org ID is required. Re-run setup and provide QUALTY_ORG_ID.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const next = {
|
|
406
|
+
org_id: orgId,
|
|
407
|
+
};
|
|
408
|
+
// Keep token in the same place MCP uses.
|
|
409
|
+
writeSharedCredentials(token);
|
|
410
|
+
writeQualtyConfig(next);
|
|
411
|
+
// Keep org in MCP-compatible per-project config too.
|
|
412
|
+
writeProjectConfig({ default_org_id: orgId });
|
|
413
|
+
// eslint-disable-next-line no-console
|
|
414
|
+
console.log(`[qualty] Saved config to ${QUALTY_CONFIG_PATH}`);
|
|
415
|
+
// eslint-disable-next-line no-console
|
|
416
|
+
console.log(`[qualty] Saved shared token to ${QUALTY_SHARED_CREDS_PATH} (used by MCP)`);
|
|
417
|
+
// eslint-disable-next-line no-console
|
|
418
|
+
console.log(`[qualty] Saved project org to ${QUALTY_PROJECT_CONFIG_PATH} (MCP-compatible)`);
|
|
419
|
+
// eslint-disable-next-line no-console
|
|
420
|
+
console.log(`[qualty] Org: ${orgName || "configured"}`);
|
|
421
|
+
// eslint-disable-next-line no-console
|
|
422
|
+
console.log(`[qualty] Token: ${maskSecret(token)}`);
|
|
423
|
+
} finally {
|
|
424
|
+
rl.close();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
177
428
|
function parseBoolean(value, defaultValue) {
|
|
178
429
|
if (value === undefined) return defaultValue;
|
|
179
430
|
if (typeof value === "boolean") return value;
|
|
@@ -519,9 +770,10 @@ function printQualtyViewLogsReport(executionId, status) {
|
|
|
519
770
|
}
|
|
520
771
|
|
|
521
772
|
async function runCi(args) {
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
const
|
|
773
|
+
const config = readQualtyConfig();
|
|
774
|
+
const apiUrl = resolveApiUrl(args, config);
|
|
775
|
+
const token = resolveToken(args, config);
|
|
776
|
+
const orgId = resolveOrgId(args, config);
|
|
525
777
|
const projectId = args.project;
|
|
526
778
|
const suiteId = args["suite-id"];
|
|
527
779
|
const explicitIds = String(args.ids || "")
|
|
@@ -671,9 +923,10 @@ async function resolveSavedJobs({ apiUrl, token, orgId, projectId, suiteId, expl
|
|
|
671
923
|
}
|
|
672
924
|
|
|
673
925
|
async function runResolve(args) {
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
const
|
|
926
|
+
const config = readQualtyConfig();
|
|
927
|
+
const apiUrl = resolveApiUrl(args, config);
|
|
928
|
+
const token = resolveToken(args, config);
|
|
929
|
+
const orgId = resolveOrgId(args, config);
|
|
677
930
|
const projectId = args.project;
|
|
678
931
|
const suiteId = args["suite-id"];
|
|
679
932
|
const explicitIds = String(args.ids || "")
|
|
@@ -713,6 +966,127 @@ async function runResolve(args) {
|
|
|
713
966
|
}
|
|
714
967
|
}
|
|
715
968
|
|
|
969
|
+
async function maybePromptSuiteIdForLocalRun({ args, apiUrl, token, orgId }) {
|
|
970
|
+
const existingSuiteId = String(args["suite-id"] || "").trim();
|
|
971
|
+
const explicitIds = String(args.ids || "")
|
|
972
|
+
.split(",")
|
|
973
|
+
.map((id) => id.trim())
|
|
974
|
+
.filter(Boolean);
|
|
975
|
+
if (existingSuiteId || explicitIds.length > 0) {
|
|
976
|
+
return existingSuiteId || "";
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const projectId = String(args.project || "").trim();
|
|
980
|
+
if (!projectId) {
|
|
981
|
+
return "";
|
|
982
|
+
}
|
|
983
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
984
|
+
return "";
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const suites = await apiRequest({
|
|
988
|
+
apiUrl,
|
|
989
|
+
token,
|
|
990
|
+
orgId,
|
|
991
|
+
path: `/api/v1/test-suites?project_id=${encodeURIComponent(projectId)}`,
|
|
992
|
+
});
|
|
993
|
+
const rows = (Array.isArray(suites) ? suites : [])
|
|
994
|
+
.map((s) => ({
|
|
995
|
+
id: String(s?.id || "").trim(),
|
|
996
|
+
name: String(s?.name || "").trim() || "Unnamed suite",
|
|
997
|
+
jobCount: Number(Array.isArray(s?.job_ids) ? s.job_ids.length : 0),
|
|
998
|
+
sequenceCount: Number(Array.isArray(s?.sequence_ids) ? s.sequence_ids.length : 0),
|
|
999
|
+
}))
|
|
1000
|
+
.filter((s) => s.id);
|
|
1001
|
+
|
|
1002
|
+
if (rows.length === 0) {
|
|
1003
|
+
throw new Error(
|
|
1004
|
+
"No test suites found for this project. Create a suite first in dashboard, or pass --ids explicitly."
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// eslint-disable-next-line no-console
|
|
1009
|
+
console.log("[qualty] No --suite-id or --ids provided. Choose a suite:");
|
|
1010
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
1011
|
+
const s = rows[i];
|
|
1012
|
+
const details = `${s.jobCount} test(s)${
|
|
1013
|
+
s.sequenceCount > 0 ? `, ${s.sequenceCount} sequence(s)` : ""
|
|
1014
|
+
}`;
|
|
1015
|
+
// eslint-disable-next-line no-console
|
|
1016
|
+
console.log(` ${i + 1}) ${s.name} (${details})`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1020
|
+
try {
|
|
1021
|
+
while (true) {
|
|
1022
|
+
const raw = String(
|
|
1023
|
+
await rl.question(`Select suite [1-${rows.length}] (default 1): `)
|
|
1024
|
+
).trim();
|
|
1025
|
+
const picked = raw ? Number(raw) : 1;
|
|
1026
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= rows.length) {
|
|
1027
|
+
const suite = rows[picked - 1];
|
|
1028
|
+
// eslint-disable-next-line no-console
|
|
1029
|
+
console.log(`[qualty] Selected suite: ${suite.name}`);
|
|
1030
|
+
return suite.id;
|
|
1031
|
+
}
|
|
1032
|
+
// eslint-disable-next-line no-console
|
|
1033
|
+
console.log(`[qualty] Please enter a number from 1 to ${rows.length}.`);
|
|
1034
|
+
}
|
|
1035
|
+
} finally {
|
|
1036
|
+
rl.close();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function maybePromptProjectIdForLocalRun({ args, apiUrl, token, orgId }) {
|
|
1041
|
+
const existingProjectId = String(args.project || "").trim();
|
|
1042
|
+
if (existingProjectId) return existingProjectId;
|
|
1043
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return existingProjectId;
|
|
1044
|
+
|
|
1045
|
+
const projectsPayload = await apiRequest({
|
|
1046
|
+
apiUrl,
|
|
1047
|
+
token,
|
|
1048
|
+
orgId,
|
|
1049
|
+
path: "/api/v1/projects",
|
|
1050
|
+
});
|
|
1051
|
+
const rows = (Array.isArray(projectsPayload) ? projectsPayload : [])
|
|
1052
|
+
.map((p) => ({
|
|
1053
|
+
id: String(p?.id || "").trim(),
|
|
1054
|
+
name: String(p?.name || "").trim() || "Unnamed project",
|
|
1055
|
+
}))
|
|
1056
|
+
.filter((p) => p.id);
|
|
1057
|
+
|
|
1058
|
+
if (rows.length === 0) {
|
|
1059
|
+
throw new Error("No projects found for this account/org. Create a project first.");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// eslint-disable-next-line no-console
|
|
1063
|
+
console.log("[qualty] No --project provided. Choose a project:");
|
|
1064
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
1065
|
+
// eslint-disable-next-line no-console
|
|
1066
|
+
console.log(` ${i + 1}) ${rows[i].name}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1070
|
+
try {
|
|
1071
|
+
while (true) {
|
|
1072
|
+
const raw = String(
|
|
1073
|
+
await rl.question(`Select project [1-${rows.length}] (default 1): `)
|
|
1074
|
+
).trim();
|
|
1075
|
+
const picked = raw ? Number(raw) : 1;
|
|
1076
|
+
if (Number.isInteger(picked) && picked >= 1 && picked <= rows.length) {
|
|
1077
|
+
const project = rows[picked - 1];
|
|
1078
|
+
// eslint-disable-next-line no-console
|
|
1079
|
+
console.log(`[qualty] Selected project: ${project.name}`);
|
|
1080
|
+
return project.id;
|
|
1081
|
+
}
|
|
1082
|
+
// eslint-disable-next-line no-console
|
|
1083
|
+
console.log(`[qualty] Please enter a number from 1 to ${rows.length}.`);
|
|
1084
|
+
}
|
|
1085
|
+
} finally {
|
|
1086
|
+
rl.close();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
716
1090
|
async function main() {
|
|
717
1091
|
const args = parseArgs(process.argv);
|
|
718
1092
|
const command = args._[0];
|
|
@@ -724,14 +1098,36 @@ async function main() {
|
|
|
724
1098
|
await runConnect(args);
|
|
725
1099
|
return;
|
|
726
1100
|
}
|
|
1101
|
+
if (command === "setup") {
|
|
1102
|
+
await runSetup(args);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
727
1105
|
if (command === "run") {
|
|
1106
|
+
const config = readQualtyConfig();
|
|
728
1107
|
if (parseBoolean(args.local, false)) {
|
|
1108
|
+
const apiUrl = resolveApiUrl(args, config);
|
|
1109
|
+
const token = resolveToken(args, config);
|
|
1110
|
+
const orgId = resolveOrgId(args, config);
|
|
1111
|
+
const projectId =
|
|
1112
|
+
(await maybePromptProjectIdForLocalRun({
|
|
1113
|
+
args,
|
|
1114
|
+
apiUrl,
|
|
1115
|
+
token,
|
|
1116
|
+
orgId,
|
|
1117
|
+
})) || args.project;
|
|
1118
|
+
const suiteId =
|
|
1119
|
+
(await maybePromptSuiteIdForLocalRun({
|
|
1120
|
+
args: { ...args, project: projectId },
|
|
1121
|
+
apiUrl,
|
|
1122
|
+
token,
|
|
1123
|
+
orgId,
|
|
1124
|
+
})) || args["suite-id"];
|
|
729
1125
|
await runLocalCi({
|
|
730
|
-
apiUrl
|
|
731
|
-
token
|
|
732
|
-
orgId
|
|
733
|
-
projectId
|
|
734
|
-
suiteId
|
|
1126
|
+
apiUrl,
|
|
1127
|
+
token,
|
|
1128
|
+
orgId,
|
|
1129
|
+
projectId,
|
|
1130
|
+
suiteId,
|
|
735
1131
|
explicitIds: String(args.ids || "")
|
|
736
1132
|
.split(",")
|
|
737
1133
|
.map((id) => id.trim())
|
|
@@ -752,7 +1148,7 @@ async function main() {
|
|
|
752
1148
|
await runResolve(args);
|
|
753
1149
|
return;
|
|
754
1150
|
}
|
|
755
|
-
if (command !== "connect" && command !== "run" && command !== "resolve") {
|
|
1151
|
+
if (command !== "setup" && command !== "connect" && command !== "run" && command !== "resolve") {
|
|
756
1152
|
throw new Error(`Unknown command: ${command}`);
|
|
757
1153
|
}
|
|
758
1154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qualty",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Qualty CLI for localhost and CI test runs",
|
|
5
5
|
"bin": {
|
|
6
6
|
"qualty": "bin/qualty.js"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"playwright": "1.60.0"
|
|
14
|
+
"playwright": "1.60.0",
|
|
15
|
+
"rrweb": "^2.0.0-alpha.4"
|
|
15
16
|
}
|
|
16
17
|
}
|