@test-lab-ai/cli 0.2.7 → 0.2.8

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
@@ -123,6 +123,9 @@ Cursor user rules are set in Cursor's Settings. Restart your agent to load it.
123
123
  It installs from the public [`Test-Lab-ai/skills`](https://github.com/Test-Lab-ai/skills)
124
124
  mirror, so you always get the latest version.
125
125
 
126
+ `testlab skills update` refreshes every installed copy to the latest, and the CLI
127
+ also refreshes them automatically the first time you run it after an upgrade.
128
+
126
129
  ## For AI agents
127
130
 
128
131
  Run **`testlab examples`** for the complete JSON reference, or read `AGENTS.md`
package/bin/testlab.mjs CHANGED
@@ -23,8 +23,8 @@ 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, AGENTS, detectAgents, installSkill } from "../lib/skills.mjs"
27
- import { checkForUpdate, currentVersion } from "../lib/update-check.mjs"
26
+ import { TESTLAB_SKILLS, AGENTS, detectAgents, installSkill, installedSkillLocations } from "../lib/skills.mjs"
27
+ import { checkForUpdate, currentVersion, previousRunVersion } from "../lib/update-check.mjs"
28
28
 
29
29
  const log = (...a) => console.log(...a)
30
30
  function errExit(msg) {
@@ -51,6 +51,7 @@ Usage:
51
51
  resource (designed for AI agents)
52
52
  testlab skills install [--agent ...] Install the test-lab-plan skill (auto-detects
53
53
  Claude/Codex/Cursor; --agent claude|codex|cursor|all)
54
+ testlab skills update Refresh installed skills (also auto-runs after a CLI upgrade)
54
55
 
55
56
  Options:
56
57
  --key <tl_…> API key (else $TESTLAB_API_KEY or ~/.test-lab/config.json)
@@ -392,6 +393,46 @@ function cmdSkillsList() {
392
393
  log(`\nInstall with: testlab skills install [name] [--agent ${AGENTS.join("|")}|all]`)
393
394
  }
394
395
 
396
+ async function cmdSkillsUpdate() {
397
+ let refreshed = 0
398
+ for (const name of TESTLAB_SKILLS) {
399
+ for (const loc of installedSkillLocations(name)) {
400
+ try {
401
+ const res = await installSkill(name, loc.agent, { global: loc.global })
402
+ refreshed++
403
+ log(`✓ ${name} → ${loc.agent}${loc.global ? " (global)" : ""}: ${res.dest}`)
404
+ } catch (e) {
405
+ log(` ✗ ${name} (${loc.agent}): ${e.message}`)
406
+ }
407
+ }
408
+ }
409
+ if (refreshed === 0) log("No installed skills found here. Run `testlab skills install` first.")
410
+ else log(`\nRefreshed ${refreshed} location(s). Restart your agent to load the changes.`)
411
+ }
412
+
413
+ // After a CLI upgrade, refresh any already-installed skills in place — runs once
414
+ // per version bump (best effort). Skipped in CI / when NO_UPDATE_NOTIFIER is set.
415
+ async function maybeRefreshSkillsOnUpgrade() {
416
+ if (process.env.CI || process.env.NO_UPDATE_NOTIFIER) return
417
+ const ver = currentVersion()
418
+ const prev = previousRunVersion(ver)
419
+ if (!prev || prev === ver) return
420
+ let refreshed = 0
421
+ for (const name of TESTLAB_SKILLS) {
422
+ for (const loc of installedSkillLocations(name)) {
423
+ try {
424
+ await installSkill(name, loc.agent, { global: loc.global })
425
+ refreshed++
426
+ } catch {
427
+ /* best effort — never break the actual command */
428
+ }
429
+ }
430
+ }
431
+ if (refreshed > 0) {
432
+ process.stderr.write(`↻ CLI upgraded ${prev} → ${ver}; refreshed ${refreshed} installed skill location(s).\n`)
433
+ }
434
+ }
435
+
395
436
  async function main() {
396
437
  let parsed
397
438
  try {
@@ -414,6 +455,8 @@ async function main() {
414
455
  return
415
456
  }
416
457
 
458
+ await maybeRefreshSkillsOnUpgrade()
459
+
417
460
  switch (args[0]) {
418
461
  case "login":
419
462
  return cmdLogin(flags)
@@ -441,8 +484,9 @@ async function main() {
441
484
  return cmdExamples()
442
485
  case "skills":
443
486
  if (args[1] === "install") return cmdSkillsInstall(flags, args)
487
+ if (args[1] === "update") return cmdSkillsUpdate()
444
488
  if (args[1] === "list") return cmdSkillsList()
445
- return errExit("usage: testlab skills <install|list> [name] [--global]")
489
+ return errExit("usage: testlab skills <install|update|list> [name] [--global]")
446
490
  case "import":
447
491
  return cmdImport(flags, args)
448
492
  default:
package/lib/skills.mjs CHANGED
@@ -51,6 +51,23 @@ export function detectAgents() {
51
51
  return [...found]
52
52
  }
53
53
 
54
+ /**
55
+ * Where skill `name` is already installed — project (cwd) and global locations
56
+ * across all agents — filtered to the paths that actually exist. Used to
57
+ * refresh installed skills (on `skills update` and after a CLI upgrade).
58
+ */
59
+ export function installedSkillLocations(name) {
60
+ const home = os.homedir()
61
+ const cwd = process.cwd()
62
+ return [
63
+ { agent: "claude", global: false, path: path.join(cwd, ".claude", "skills", name) },
64
+ { agent: "codex", global: false, path: path.join(cwd, ".agents", "skills", name) },
65
+ { agent: "cursor", global: false, path: path.join(cwd, ".cursor", "rules", `${name}.mdc`) },
66
+ { agent: "claude", global: true, path: path.join(home, ".claude", "skills", name) },
67
+ { agent: "codex", global: true, path: path.join(home, ".agents", "skills", name) },
68
+ ].filter((c) => fs.existsSync(c.path))
69
+ }
70
+
54
71
  /** Resolve the install target (base dir + format) for one agent. */
55
72
  export function agentTarget(agent, { global } = {}) {
56
73
  const home = os.homedir()
@@ -2,7 +2,7 @@
2
2
  * Best-effort "update available" notice.
3
3
  *
4
4
  * Design constraints: never block, slow, or break a command. It reads a cached
5
- * latest-version (checked against the npm registry at most once/day), prints a
5
+ * latest-version (checked against the npm registry at most every few hours), prints a
6
6
  * one-line notice to STDERR when the running version is behind, and kicks off a
7
7
  * background refresh for next time. Any failure (offline, slow/forbidden
8
8
  * registry) is swallowed. Suppressed when stderr isn't a TTY (pipes / CI /
@@ -15,7 +15,7 @@ import path from "node:path"
15
15
  const PKG = "@test-lab-ai/cli"
16
16
  const REGISTRY = "https://registry.npmjs.org/@test-lab-ai%2Fcli"
17
17
  const CACHE = path.join(os.homedir(), ".test-lab", "update-check.json")
18
- const DAY = 24 * 60 * 60 * 1000
18
+ const CHECK_TTL = 3 * 60 * 60 * 1000 // re-check npm at most every 3h
19
19
 
20
20
  export function currentVersion() {
21
21
  try {
@@ -70,16 +70,29 @@ export function checkForUpdate() {
70
70
  const notice = updateNotice(current, cache.latest)
71
71
  if (notice) process.stderr.write(notice + "\n")
72
72
 
73
- if (!cache.lastCheck || Date.now() - cache.lastCheck > DAY) {
73
+ if (!cache.lastCheck || Date.now() - cache.lastCheck > CHECK_TTL) {
74
74
  const ctrl = new AbortController()
75
75
  const timer = setTimeout(() => ctrl.abort(), 2000)
76
76
  fetch(REGISTRY, { signal: ctrl.signal, headers: { Accept: "application/vnd.npm.install-v1+json" } })
77
77
  .then((r) => (r.ok ? r.json() : null))
78
78
  .then((j) => {
79
79
  const latest = j && j["dist-tags"] && j["dist-tags"].latest
80
- if (latest) writeCache({ lastCheck: Date.now(), latest })
80
+ if (latest) writeCache({ ...cache, lastCheck: Date.now(), latest })
81
81
  })
82
82
  .catch(() => {})
83
83
  .finally(() => clearTimeout(timer))
84
84
  }
85
85
  }
86
+
87
+ /**
88
+ * Record the running CLI version and return the version seen on the PREVIOUS
89
+ * run (null on first run). Lets the CLI notice "I was just upgraded" so it can
90
+ * refresh installed skills. Merges into the shared cache so it doesn't clobber
91
+ * the update-check state.
92
+ */
93
+ export function previousRunVersion(current) {
94
+ const cache = readCache()
95
+ const prev = cache.cliVersion || null
96
+ if (prev !== current) writeCache({ ...cache, cliVersion: current })
97
+ return prev
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-lab-ai/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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": {