@test-lab-ai/cli 0.2.12 → 0.2.13

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/testlab.mjs CHANGED
@@ -23,7 +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, AGENTS, detectAgents, installSkill, installedSkillLocations } from "../lib/skills.mjs"
26
+ import { TESTLAB_SKILLS, AGENTS, detectAgents, installSkill, installedSkillLocations, installedSkillTargets } from "../lib/skills.mjs"
27
27
  import { checkForUpdate, currentVersion, previousRunVersion } from "../lib/update-check.mjs"
28
28
 
29
29
  const log = (...a) => console.log(...a)
@@ -462,44 +462,73 @@ function cmdSkillsList() {
462
462
  log(`\nInstall with: testlab skills install [name] [--agent ${AGENTS.join("|")}|all]`)
463
463
  }
464
464
 
465
- async function cmdSkillsUpdate() {
466
- let refreshed = 0
465
+ // Install EVERY current skill at every location where any test-lab skill is
466
+ // already present — refreshing existing copies AND adding skills shipped since
467
+ // the user last installed, so an update delivers the latest skill SET (not just
468
+ // fresher copies of the old set). `installedSkillTargets()` is the union of
469
+ // (agent, scope) homes; a brand-new skill has no install locations of its own
470
+ // yet, which is exactly why the old per-skill loop never picked it up. Returns
471
+ // one result per (skill, location), with `isNew` flagging a first-time install.
472
+ async function refreshInstalledSkills() {
473
+ const had = new Set()
467
474
  for (const name of TESTLAB_SKILLS) {
468
- for (const loc of installedSkillLocations(name)) {
475
+ for (const loc of installedSkillLocations(name)) had.add(`${name}:${loc.agent}:${loc.global}`)
476
+ }
477
+ const results = []
478
+ for (const { agent, global } of installedSkillTargets()) {
479
+ for (const name of TESTLAB_SKILLS) {
480
+ const isNew = !had.has(`${name}:${agent}:${global}`)
469
481
  try {
470
- const res = await installSkill(name, loc.agent, { global: loc.global })
471
- refreshed++
472
- log(`✓ ${name} → ${loc.agent}${loc.global ? " (global)" : ""}: ${res.dest}`)
482
+ const res = await installSkill(name, agent, { global })
483
+ results.push({ name, agent, global, isNew, ok: true, dest: res.dest })
473
484
  } catch (e) {
474
- log(` ✗ ${name} (${loc.agent}): ${e.message}`)
485
+ results.push({ name, agent, global, isNew, ok: false, error: e.message })
475
486
  }
476
487
  }
477
488
  }
478
- if (refreshed === 0) log("No installed skills found here. Run `testlab skills install` first.")
479
- else log(`\nRefreshed ${refreshed} location(s). Restart your agent to load the changes.`)
489
+ return results
490
+ }
491
+
492
+ async function cmdSkillsUpdate() {
493
+ const results = await refreshInstalledSkills()
494
+ if (results.length === 0) {
495
+ log("No installed skills found here. Run `testlab skills install` first.")
496
+ return
497
+ }
498
+ for (const r of results) {
499
+ const where = `${r.agent}${r.global ? " (global)" : ""}`
500
+ if (!r.ok) log(` ✗ ${r.name} (${where}): ${r.error}`)
501
+ else log(`${r.isNew ? "+ installed" : "✓ refreshed"} ${r.name} → ${where}: ${r.dest}`)
502
+ }
503
+ const ok = results.filter((r) => r.ok)
504
+ const added = ok.filter((r) => r.isNew).length
505
+ const bits = []
506
+ if (ok.length - added) bits.push(`${ok.length - added} refreshed`)
507
+ if (added) bits.push(`${added} newly installed`)
508
+ log(`\n${bits.join(", ") || "Nothing to update"}. Restart your agent to load the changes.`)
480
509
  }
481
510
 
482
- // After a CLI upgrade, refresh any already-installed skills in place runs once
483
- // per version bump (best effort). Skipped in CI / when NO_UPDATE_NOTIFIER is set.
511
+ // After a CLI upgrade, refresh installed skills in place AND add any newly
512
+ // shipped ones — runs once per version bump (best effort). Skipped in CI / when
513
+ // NO_UPDATE_NOTIFIER is set.
484
514
  async function maybeRefreshSkillsOnUpgrade() {
485
515
  if (process.env.CI || process.env.NO_UPDATE_NOTIFIER) return
486
516
  const ver = currentVersion()
487
517
  const prev = previousRunVersion(ver)
488
518
  if (!prev || prev === ver) return
489
- let refreshed = 0
490
- for (const name of TESTLAB_SKILLS) {
491
- for (const loc of installedSkillLocations(name)) {
492
- try {
493
- await installSkill(name, loc.agent, { global: loc.global })
494
- refreshed++
495
- } catch {
496
- /* best effort — never break the actual command */
497
- }
498
- }
499
- }
500
- if (refreshed > 0) {
501
- process.stderr.write(`↻ CLI upgraded ${prev} → ${ver}; refreshed ${refreshed} installed skill location(s).\n`)
519
+ let ok
520
+ try {
521
+ ok = (await refreshInstalledSkills()).filter((r) => r.ok)
522
+ } catch {
523
+ return // best effort never break the actual command
502
524
  }
525
+ if (ok.length === 0) return
526
+ const added = ok.filter((r) => r.isNew).length
527
+ const refreshed = ok.length - added
528
+ const bits = []
529
+ if (refreshed) bits.push(`refreshed ${refreshed}`)
530
+ if (added) bits.push(`installed ${added} new`)
531
+ process.stderr.write(`↻ CLI upgraded ${prev} → ${ver}; ${bits.join(", ")} skill location(s).\n`)
503
532
  }
504
533
 
505
534
  async function main() {
package/lib/skills.mjs CHANGED
@@ -70,6 +70,23 @@ export function installedSkillLocations(name) {
70
70
  ].filter((c) => fs.existsSync(c.path))
71
71
  }
72
72
 
73
+ /**
74
+ * The distinct (agent, scope) locations where AT LEAST ONE test-lab skill is
75
+ * currently installed. `skills update` and the on-upgrade refresh install EVERY
76
+ * current skill at each of these, so a skill added in a later CLI release lands
77
+ * wherever the user already keeps test-lab skills — not just fresher copies of
78
+ * the skills that happened to be installed before.
79
+ */
80
+ export function installedSkillTargets() {
81
+ const seen = new Map()
82
+ for (const name of TESTLAB_SKILLS) {
83
+ for (const loc of installedSkillLocations(name)) {
84
+ seen.set(`${loc.agent}:${loc.global}`, { agent: loc.agent, global: loc.global })
85
+ }
86
+ }
87
+ return [...seen.values()]
88
+ }
89
+
73
90
  /** Resolve the install target (base dir + format) for one agent. */
74
91
  export function agentTarget(agent, { global } = {}) {
75
92
  const home = os.homedir()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@test-lab-ai/cli",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
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": {