@test-lab-ai/cli 0.2.1 → 0.2.2

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 CHANGED
@@ -100,6 +100,19 @@ in the file doesn't matter. Re-running is safe for credentials/labels/fixtures
100
100
  - `{{data.FIXTURE.FIELD}}` — a value from a data fixture (generated test data).
101
101
  - `{{run.shortId}}` — a unique per-run id (for unique emails, names, etc.).
102
102
 
103
+ ## Install the test-lab-plan skill (Claude Code)
104
+
105
+ The companion skill that *writes* test-lab plans — so an AI agent can design a
106
+ plan, then this CLI imports it:
107
+
108
+ ```bash
109
+ testlab skills install # into ./.claude/skills (this project)
110
+ testlab skills install --global # into ~/.claude/skills (all projects)
111
+ ```
112
+
113
+ Restart Claude Code to load it. It installs from the public skills mirror, so
114
+ you always get the latest version. (Targets Claude Code's `.claude/skills/` layout.)
115
+
103
116
  ## For AI agents
104
117
 
105
118
  Run **`testlab examples`** for the complete JSON reference, or read `AGENTS.md`
package/bin/testlab.mjs CHANGED
@@ -23,6 +23,7 @@ import { apiFetch } from "../lib/api.mjs"
23
23
  import { loadImportFile, runImport } from "../lib/import.mjs"
24
24
  import { browserLogin } from "../lib/login.mjs"
25
25
  import { EXAMPLES_TEXT } from "../lib/examples.mjs"
26
+ import { TESTLAB_SKILLS, resolveSkillsDir, installSkill } from "../lib/skills.mjs"
26
27
 
27
28
  const log = (...a) => console.log(...a)
28
29
  function errExit(msg) {
@@ -44,6 +45,7 @@ Usage:
44
45
  testlab data create -f fixture.json Create a data fixture ({{data.KEY.FIELD}})
45
46
  testlab examples Print the full JSON reference for every
46
47
  resource (designed for AI agents)
48
+ testlab skills install [--global] Install the test-lab-plan skill into Claude Code
47
49
 
48
50
  Options:
49
51
  --key <tl_…> API key (else $TESTLAB_API_KEY or ~/.test-lab/config.json)
@@ -66,6 +68,8 @@ function parse() {
66
68
  "dry-run": { type: "boolean" },
67
69
  stdin: { type: "boolean" },
68
70
  force: { type: "boolean" },
71
+ global: { type: "boolean" },
72
+ dir: { type: "string" },
69
73
  help: { type: "boolean", short: "h" },
70
74
  },
71
75
  })
@@ -248,6 +252,26 @@ function cmdExamples() {
248
252
  log(EXAMPLES_TEXT)
249
253
  }
250
254
 
255
+ async function cmdSkillsInstall(flags, args) {
256
+ const requested = args[2] ? [args[2]] : TESTLAB_SKILLS
257
+ const targetDir = resolveSkillsDir({ global: flags.global, dir: flags.dir })
258
+ for (const name of requested) {
259
+ try {
260
+ const res = await installSkill(name, targetDir)
261
+ log(`✓ Installed ${res.name} (${res.count} file${res.count === 1 ? "" : "s"}) → ${res.dir}`)
262
+ } catch (e) {
263
+ errExit(`failed to install ${name}: ${e.message}`)
264
+ }
265
+ }
266
+ log(`\nRestart Claude Code (start a new session) to load the skill.`)
267
+ }
268
+
269
+ function cmdSkillsList() {
270
+ log(`Available test-lab skills:`)
271
+ for (const s of TESTLAB_SKILLS) log(` ${s}`)
272
+ log(`\nInstall with: testlab skills install [name] [--global]`)
273
+ }
274
+
251
275
  async function main() {
252
276
  let parsed
253
277
  try {
@@ -281,6 +305,10 @@ async function main() {
281
305
  return errExit("usage: testlab data <list|create>")
282
306
  case "examples":
283
307
  return cmdExamples()
308
+ case "skills":
309
+ if (args[1] === "install") return cmdSkillsInstall(flags, args)
310
+ if (args[1] === "list") return cmdSkillsList()
311
+ return errExit("usage: testlab skills <install|list> [name] [--global]")
284
312
  case "import":
285
313
  return cmdImport(flags, args)
286
314
  default:
package/lib/login.mjs CHANGED
@@ -38,7 +38,7 @@ function openBrowser(url) {
38
38
  }
39
39
  }
40
40
 
41
- export function browserLogin(apiUrl, { timeoutMs = 180000 } = {}) {
41
+ export function browserLogin(apiUrl, { timeoutMs = 600000 } = {}) {
42
42
  const state = crypto.randomBytes(16).toString("hex")
43
43
 
44
44
  return new Promise((resolve, reject) => {
@@ -80,13 +80,14 @@ export function browserLogin(apiUrl, { timeoutMs = 180000 } = {}) {
80
80
 
81
81
  const timer = setTimeout(() => {
82
82
  server.close()
83
- reject(new Error("login timed out after 3 minutes"))
83
+ reject(new Error("login timed out after 10 minutes"))
84
84
  }, timeoutMs)
85
85
 
86
86
  server.listen(0, "127.0.0.1", () => {
87
87
  const { port } = server.address()
88
88
  const authUrl = `${apiUrl}/cli/authorize?state=${state}&port=${port}`
89
89
  console.log(`Opening your browser to authorize the CLI:\n ${authUrl}\n`)
90
+ console.log(`Waiting for you to approve in the browser (up to 10 minutes)…\n`)
90
91
  openBrowser(authUrl)
91
92
  })
92
93
  })
package/lib/skills.mjs ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Install test-lab skills (e.g. test-lab-plan) into a local agent's skills
3
+ * directory by fetching them from the public Test-Lab-ai/skills mirror.
4
+ *
5
+ * Zero-dep: global fetch + node:fs. Targets Claude Code's
6
+ * .claude/skills/<name>/ layout — project dir by default, ~/.claude with
7
+ * --global, or an explicit --dir. The mirror is the single source of truth,
8
+ * so this always installs the latest published skill.
9
+ */
10
+ import fs from "node:fs"
11
+ import os from "node:os"
12
+ import path from "node:path"
13
+
14
+ const MIRROR = "Test-Lab-ai/skills"
15
+ const MIRROR_BRANCH = "main"
16
+
17
+ // Skills published by test-lab. Add new skill directory names here.
18
+ export const TESTLAB_SKILLS = ["test-lab-plan"]
19
+
20
+ export function resolveSkillsDir({ global, dir } = {}) {
21
+ if (dir) return path.resolve(dir)
22
+ if (global) return path.join(os.homedir(), ".claude", "skills")
23
+ return path.join(process.cwd(), ".claude", "skills")
24
+ }
25
+
26
+ const UA = { "User-Agent": "test-lab-cli" }
27
+
28
+ // List the blob paths under skills/<name>/ in the mirror's tree.
29
+ async function listSkillFiles(name) {
30
+ const r = await fetch(
31
+ `https://api.github.com/repos/${MIRROR}/git/trees/${MIRROR_BRANCH}?recursive=1`,
32
+ { headers: { ...UA, Accept: "application/vnd.github+json" } }
33
+ )
34
+ if (!r.ok) throw new Error(`could not list ${MIRROR} (${r.status})`)
35
+ const tree = await r.json()
36
+ const prefix = `skills/${name}/`
37
+ return (tree.tree || [])
38
+ .filter((e) => e.type === "blob" && typeof e.path === "string" && e.path.startsWith(prefix))
39
+ .map((e) => e.path)
40
+ }
41
+
42
+ /** Fetch every file of one skill from the mirror and write it under targetDir/<name>/. */
43
+ export async function installSkill(name, targetDir) {
44
+ const files = await listSkillFiles(name)
45
+ if (files.length === 0) throw new Error(`skill "${name}" not found in ${MIRROR}`)
46
+ const prefix = `skills/${name}/`
47
+ let count = 0
48
+ for (const p of files) {
49
+ const rel = p.slice(prefix.length)
50
+ const raw = await fetch(`https://raw.githubusercontent.com/${MIRROR}/${MIRROR_BRANCH}/${p}`, {
51
+ headers: UA,
52
+ })
53
+ if (!raw.ok) throw new Error(`could not fetch ${p} (${raw.status})`)
54
+ const body = await raw.text()
55
+ const dest = path.join(targetDir, name, rel)
56
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
57
+ fs.writeFileSync(dest, body)
58
+ count++
59
+ }
60
+ return { name, count, dir: path.join(targetDir, name) }
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-lab-ai/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Import existing test plans into test-lab.ai from the command line (or an AI agent).",
5
5
  "type": "module",
6
6
  "bin": {