@test-lab-ai/cli 0.2.6 → 0.2.7
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/AGENTS.md +3 -1
- package/bin/testlab.mjs +65 -2
- package/lib/examples.mjs +2 -0
- package/lib/import.mjs +2 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -33,7 +33,9 @@ npx @test-lab-ai/cli --help
|
|
|
33
33
|
- If the user's repo has a test-lab plan skill/format, prefer it.
|
|
34
34
|
3. **Write** the plans to a JSON file (or a directory of `*.json`).
|
|
35
35
|
4. **Run** `testlab import <path>`. Use `--dry-run` first to show the user the
|
|
36
|
-
creation order without writing anything.
|
|
36
|
+
creation order without writing anything. To file the plans under a project,
|
|
37
|
+
run `testlab projects list` and pass `--project <id|name>` (auto-picked if
|
|
38
|
+
there is only one; omit for account-level).
|
|
37
39
|
|
|
38
40
|
Authentication: the user runs `testlab login` once (browser), or you set
|
|
39
41
|
`TESTLAB_API_KEY` in the environment for a fully headless run.
|
package/bin/testlab.mjs
CHANGED
|
@@ -41,6 +41,7 @@ Usage:
|
|
|
41
41
|
(credentials, labels, fixtures, plans)
|
|
42
42
|
testlab plans list List your test plans
|
|
43
43
|
testlab plans create -f plan.json Create one plan from JSON
|
|
44
|
+
testlab projects list List your projects
|
|
44
45
|
testlab credentials set <key> --value <value> Set a credential ({{credentials.<key>}})
|
|
45
46
|
testlab credentials list List credential keys (values never shown)
|
|
46
47
|
testlab labels list List your labels
|
|
@@ -56,6 +57,7 @@ Options:
|
|
|
56
57
|
--stdin Read the value (key/credential) from stdin
|
|
57
58
|
--force (login) re-authenticate even if a stored key still works
|
|
58
59
|
--dry-run (import) validate + print plan order without writing
|
|
60
|
+
--project <id|name> (import/plans) assign plans to a project, or "none" for account-level
|
|
59
61
|
--version, -v Print the installed CLI version
|
|
60
62
|
|
|
61
63
|
Get a key at https://test-lab.ai/admin/settings/api-keys · run \`testlab examples\` for JSON shapes`
|
|
@@ -75,6 +77,7 @@ function parse() {
|
|
|
75
77
|
force: { type: "boolean" },
|
|
76
78
|
global: { type: "boolean" },
|
|
77
79
|
agent: { type: "string" },
|
|
80
|
+
project: { type: "string" },
|
|
78
81
|
version: { type: "boolean", short: "v" },
|
|
79
82
|
help: { type: "boolean", short: "h" },
|
|
80
83
|
},
|
|
@@ -185,7 +188,8 @@ async function cmdPlansCreate(flags) {
|
|
|
185
188
|
} else {
|
|
186
189
|
errExit("provide -f <plan.json>, or both --name and --prompt")
|
|
187
190
|
}
|
|
188
|
-
const
|
|
191
|
+
const projectId = plan.projectId ?? (await resolveProjectId(apiUrl, apiKey, flags))
|
|
192
|
+
const r = await apiFetch(apiUrl, apiKey, "POST", "/api/v1/test-plans", { ...plan, projectId })
|
|
189
193
|
if (!r.ok) errExit(`${r.status}: ${r.json?.error || ""}`)
|
|
190
194
|
log(`✓ Created plan #${r.json.testPlan.id}: ${r.json.testPlan.name}`)
|
|
191
195
|
}
|
|
@@ -231,6 +235,60 @@ async function cmdLabelsList(flags) {
|
|
|
231
235
|
log(`\n${labels.length} label(s)`)
|
|
232
236
|
}
|
|
233
237
|
|
|
238
|
+
// Resolve which project imported plans go into.
|
|
239
|
+
// --project none -> null (account-level, no project)
|
|
240
|
+
// --project <id|name> -> that project
|
|
241
|
+
// (no flag) -> 0 projects: null; 1 project: auto; many: prompt (TTY)
|
|
242
|
+
// or, for an agent/CI (no TTY), error listing the choices.
|
|
243
|
+
async function resolveProjectId(apiUrl, apiKey, flags) {
|
|
244
|
+
const flag = flags.project
|
|
245
|
+
if (flag === "none" || flag === "null") return null
|
|
246
|
+
const r = await apiFetch(apiUrl, apiKey, "GET", "/api/v1/projects")
|
|
247
|
+
if (!r.ok) errExit(`could not list projects (${r.status}): ${r.json?.error || ""}`)
|
|
248
|
+
const projects = r.json?.projects || []
|
|
249
|
+
if (flag) {
|
|
250
|
+
const match = /^\d+$/.test(flag)
|
|
251
|
+
? projects.find((p) => p.id === parseInt(flag, 10))
|
|
252
|
+
: projects.find((p) => p.name.toLowerCase() === flag.toLowerCase())
|
|
253
|
+
if (!match) {
|
|
254
|
+
errExit(`project "${flag}" not found. Available: ${projects.map((p) => p.name).join(", ") || "(none)"}`)
|
|
255
|
+
}
|
|
256
|
+
return match.id
|
|
257
|
+
}
|
|
258
|
+
if (projects.length === 0) return null
|
|
259
|
+
if (projects.length === 1) {
|
|
260
|
+
log(`Using project: ${projects[0].name} (#${projects[0].id})`)
|
|
261
|
+
return projects[0].id
|
|
262
|
+
}
|
|
263
|
+
if (!process.stdin.isTTY) {
|
|
264
|
+
errExit(
|
|
265
|
+
`You have ${projects.length} projects — pick one with --project <id|name> (or --project none):\n` +
|
|
266
|
+
projects.map((p) => ` ${p.id} ${p.name}`).join("\n")
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
log(`Which project should these plans go in?`)
|
|
270
|
+
projects.forEach((p, i) => log(` ${i + 1}. ${p.name} (#${p.id})`))
|
|
271
|
+
log(` 0. None (account-level)`)
|
|
272
|
+
const ans = await promptLine("Project [number]: ")
|
|
273
|
+
if (ans === "0") return null
|
|
274
|
+
const idx = parseInt(ans, 10)
|
|
275
|
+
if (idx >= 1 && idx <= projects.length) return projects[idx - 1].id
|
|
276
|
+
errExit("invalid selection")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function cmdProjectsList(flags) {
|
|
280
|
+
const { apiKey, apiUrl } = requireAuth(flags)
|
|
281
|
+
const r = await apiFetch(apiUrl, apiKey, "GET", "/api/v1/projects")
|
|
282
|
+
if (!r.ok) errExit(`${r.status}: ${r.json?.error || ""}`)
|
|
283
|
+
const projects = r.json?.projects || []
|
|
284
|
+
if (projects.length === 0) {
|
|
285
|
+
log("No projects yet. Imported plans stay account-level unless you create a project in the dashboard.")
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
for (const p of projects) log(` #${p.id} ${p.name}`)
|
|
289
|
+
log(`\n${projects.length} project(s)`)
|
|
290
|
+
}
|
|
291
|
+
|
|
234
292
|
async function cmdImport(flags, args) {
|
|
235
293
|
const target = args[1]
|
|
236
294
|
if (!target) errExit("usage: testlab import <path> [--dry-run]")
|
|
@@ -243,8 +301,10 @@ async function cmdImport(flags, args) {
|
|
|
243
301
|
const dryRun = flags["dry-run"]
|
|
244
302
|
// --dry-run validates locally and never calls the API, so it doesn't need auth.
|
|
245
303
|
const { apiKey, apiUrl } = dryRun ? resolveAuth(flags) : requireAuth(flags)
|
|
304
|
+
// Resolve the target project (auto-pick / prompt / --project) for real imports only.
|
|
305
|
+
const projectId = dryRun || loaded.plans.length === 0 ? null : await resolveProjectId(apiUrl, apiKey, flags)
|
|
246
306
|
log(`Importing from ${target}: ${loaded.plans.length} plan(s), ${loaded.credentials.length} credential(s), ${loaded.labels.length} label(s), ${loaded.fixtures.length} fixture(s)`)
|
|
247
|
-
const res = await runImport({ apiUrl, apiKey, ...loaded, dryRun, log })
|
|
307
|
+
const res = await runImport({ apiUrl, apiKey, projectId, ...loaded, dryRun, log })
|
|
248
308
|
if (!res.ok && !res.dryRun) process.exit(1)
|
|
249
309
|
}
|
|
250
310
|
|
|
@@ -363,6 +423,9 @@ async function main() {
|
|
|
363
423
|
if (args[1] === "list") return cmdPlansList(flags)
|
|
364
424
|
if (args[1] === "create") return cmdPlansCreate(flags)
|
|
365
425
|
return errExit("usage: testlab plans <list|create>")
|
|
426
|
+
case "projects":
|
|
427
|
+
if (args[1] === "list") return cmdProjectsList(flags)
|
|
428
|
+
return errExit("usage: testlab projects list")
|
|
366
429
|
case "credentials":
|
|
367
430
|
if (args[1] === "set") return cmdCredentialsSet(flags, args)
|
|
368
431
|
if (args[1] === "list") return cmdCredentialsList(flags)
|
package/lib/examples.mjs
CHANGED
|
@@ -71,6 +71,7 @@ JSON (under "plans"):
|
|
|
71
71
|
"agentType": "functional", // functional|accessibility|uiux|exploratory|performance|security
|
|
72
72
|
"devices": ["Desktop Chrome"], // or ["iPhone 15 Pro"]
|
|
73
73
|
"labels": ["smoke", "auth"],
|
|
74
|
+
"projectId": 12, // optional: assign to a project (else account-level)
|
|
74
75
|
"failOnPreStepFailure": true
|
|
75
76
|
}
|
|
76
77
|
Pre-steps (run another plan first, sharing browser state):
|
|
@@ -80,6 +81,7 @@ Pre-steps (run another plan first, sharing browser state):
|
|
|
80
81
|
{ "testPlanId": 42 } // an existing plan by id
|
|
81
82
|
]
|
|
82
83
|
Rules: name <=200 chars; prompt <=32KB; <=25 labels; <=25 preSteps.
|
|
84
|
+
Projects: omit projectId to import account-level. Run "testlab projects list" for ids; "testlab import --project <id|name>" (or --project none) sets it for the whole import, and is auto-picked when you have exactly one.
|
|
83
85
|
|
|
84
86
|
═══════════════════════════════════════════════════════════════════════════
|
|
85
87
|
IMPORT BUNDLE — create everything at once: testlab import ./bundle.json
|
package/lib/import.mjs
CHANGED
|
@@ -90,6 +90,7 @@ export async function runImport({
|
|
|
90
90
|
labels = [],
|
|
91
91
|
fixtures = [],
|
|
92
92
|
plans = [],
|
|
93
|
+
projectId = null,
|
|
93
94
|
dryRun = false,
|
|
94
95
|
log = console.log,
|
|
95
96
|
}) {
|
|
@@ -187,6 +188,7 @@ export async function runImport({
|
|
|
187
188
|
cookies: plan.cookies,
|
|
188
189
|
headers: plan.headers,
|
|
189
190
|
failOnPreStepFailure: plan.failOnPreStepFailure,
|
|
191
|
+
projectId: plan.projectId ?? projectId, // per-plan id wins, else the resolved import-wide project
|
|
190
192
|
preSteps: normalizePreSteps(plan.preSteps, refToId),
|
|
191
193
|
}
|
|
192
194
|
const r = await apiFetch(apiUrl, apiKey, "POST", "/api/v1/test-plans", payload)
|