@testspectra/skills 1.1.0-rc.0
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/LICENSE.md +48 -0
- package/README.md +78 -0
- package/bin/spectra-skills.js +7 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +44 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +22 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +26 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +30 -0
- package/dist/detector.d.ts +35 -0
- package/dist/detector.js +129 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +119 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +48 -0
- package/dist/skills/fixtures-data/SKILL.md +109 -0
- package/dist/skills/lifecycle-hooks/SKILL.md +114 -0
- package/dist/skills/matchers-and-assertions/SKILL.md +123 -0
- package/dist/skills/network-interception/SKILL.md +115 -0
- package/dist/skills/page-objects-and-selectors/SKILL.md +107 -0
- package/dist/skills/shared-steps-and-actions/SKILL.md +184 -0
- package/dist/skills/spec-and-suite-authoring/SKILL.md +132 -0
- package/dist/skills/workspace-structure/SKILL.md +181 -0
- package/package.json +59 -0
- package/skills/fixtures-data/SKILL.md +109 -0
- package/skills/lifecycle-hooks/SKILL.md +114 -0
- package/skills/matchers-and-assertions/SKILL.md +123 -0
- package/skills/network-interception/SKILL.md +115 -0
- package/skills/page-objects-and-selectors/SKILL.md +107 -0
- package/skills/shared-steps-and-actions/SKILL.md +184 -0
- package/skills/spec-and-suite-authoring/SKILL.md +132 -0
- package/skills/workspace-structure/SKILL.md +181 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# TestSpectra Commercial Software License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TestSpectra. All rights reserved.
|
|
4
|
+
|
|
5
|
+
IMPORTANT NOTICE: This software is NOT open-source. Although distribution archives,
|
|
6
|
+
npm packages, and editor extensions are publicly downloadable, installation,
|
|
7
|
+
execution, and use of the Software are strictly conditioned upon obtaining a valid
|
|
8
|
+
Commercial License or an authorized Evaluation/Trial License from TestSpectra.
|
|
9
|
+
|
|
10
|
+
1. GRANT OF LICENSE
|
|
11
|
+
Subject to payment of applicable license fees or an active authorized evaluation
|
|
12
|
+
period, TestSpectra grants you a non-exclusive, non-transferable, revocable license
|
|
13
|
+
to install and execute the Software solely for authoring, debugging, and executing
|
|
14
|
+
automated software tests.
|
|
15
|
+
|
|
16
|
+
2. LICENSE ACTIVATION & ENFORCEMENT
|
|
17
|
+
- Use of the Software requires activation via a valid License Key, authorized CI token,
|
|
18
|
+
or active TestSpectra Cloud account.
|
|
19
|
+
- Any attempt to circumvent, disable, modify, reverse engineer, or tamper with the
|
|
20
|
+
license verification mechanisms, cryptographic signatures, or binary checks is
|
|
21
|
+
a material breach of this Agreement and automatically terminates your license.
|
|
22
|
+
|
|
23
|
+
3. RESTRICTIONS
|
|
24
|
+
You shall not, and shall not permit any third party to:
|
|
25
|
+
- Decompile, disassemble, reverse engineer, or derive source code from the binaries,
|
|
26
|
+
compiled scripts, or libraries of the Software.
|
|
27
|
+
- Sublicense, sell, rent, lease, distribute, or host the Software as a service for third parties.
|
|
28
|
+
- Re-brand, white-label, or package the Software or any of its components as part of a
|
|
29
|
+
competing commercial QA, testing, or developer tooling product.
|
|
30
|
+
- Modify, alter, or remove any proprietary notices, copyright labels, or license headers.
|
|
31
|
+
|
|
32
|
+
4. INTELLECTUAL PROPERTY
|
|
33
|
+
The Software, including its source code, architecture, Rust core engine, algorithms,
|
|
34
|
+
matchers, extensions, interfaces, and documentation, represents valuable intellectual
|
|
35
|
+
property and trade secrets of TestSpectra.
|
|
36
|
+
|
|
37
|
+
5. TERMINATION
|
|
38
|
+
This license is effective until terminated. Your rights under this license will terminate
|
|
39
|
+
automatically without notice if you fail to comply with any of its terms, or upon expiration
|
|
40
|
+
of your active subscription or trial term. Upon termination, you must cease all use of
|
|
41
|
+
the Software and delete all copies in your possession.
|
|
42
|
+
|
|
43
|
+
6. DISCLAIMER OF WARRANTY & LIMITATION OF LIABILITY
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
45
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
46
|
+
PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
47
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
48
|
+
ARISING FROM THE USE OF OR INABILITY TO USE THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @testspectra/skills
|
|
2
|
+
|
|
3
|
+
Modular AI agent skills installer & manager for standardizing **TestSpectra** script authoring —
|
|
4
|
+
specs, Page Objects, shared steps, custom actions, lifecycle hooks, fixtures, and the matcher
|
|
5
|
+
vocabulary.
|
|
6
|
+
|
|
7
|
+
Distributed the same way as [`@testspectra/matchers`](../matchers): as a standalone npm package
|
|
8
|
+
with its own CLI, installable independently of the main `@testspectra/cli`. When `@testspectra/cli`
|
|
9
|
+
is already installed in a workspace, the exact same commands are also available unified under
|
|
10
|
+
`spectra skills` — no separate install required.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# 1. One-time setup: pick agent(s) + which skills to install, in one guided flow
|
|
18
|
+
npx @testspectra/skills init # or: spectra skills init
|
|
19
|
+
|
|
20
|
+
# 2. Standalone binary (works with zero other TestSpectra packages installed)
|
|
21
|
+
npx @testspectra/skills list
|
|
22
|
+
npx @testspectra/skills add
|
|
23
|
+
npx @testspectra/skills add workspace-structure spec-and-suite-authoring matchers-and-assertions
|
|
24
|
+
npx @testspectra/skills update
|
|
25
|
+
npx @testspectra/skills remove network-interception
|
|
26
|
+
|
|
27
|
+
# Unified under the main CLI (equivalent — use whichever is already installed)
|
|
28
|
+
spectra skills list
|
|
29
|
+
spectra skills add
|
|
30
|
+
spectra skills add workspace-structure spec-and-suite-authoring matchers-and-assertions
|
|
31
|
+
spectra skills update
|
|
32
|
+
spectra skills remove network-interception
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`init` is a two-step guided flow:
|
|
36
|
+
|
|
37
|
+
1. **Pick agent(s)** — which AI agent(s) the project uses (Claude Code, OpenCode, Kilo, Universal
|
|
38
|
+
`.agents`, or the global Gemini CLI). Saved to `.spectra-skills.json` at the workspace root, so
|
|
39
|
+
every later `list`/`add`/`update`/`remove` call reads it automatically — no need to re-detect or
|
|
40
|
+
re-specify agents each time. Without `init`, commands fall back to auto-detecting only agent
|
|
41
|
+
directories that already exist in the target workspace.
|
|
42
|
+
2. **Pick skills** — a multiselect (all 8 pre-checked) of which skills to install right away for
|
|
43
|
+
the agent(s) just selected. Deselect what you don't want, or submit empty to skip installing
|
|
44
|
+
anything yet (you can always run `add` later).
|
|
45
|
+
|
|
46
|
+
A one-off `--agent <ids>` (or `-a` on `spectra skills`) flag on `list`/`add`/`update`/`remove`
|
|
47
|
+
overrides the saved selection for a single invocation.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Available Skills
|
|
52
|
+
|
|
53
|
+
| Skill | Covers |
|
|
54
|
+
| :--- | :--- |
|
|
55
|
+
| `workspace-structure` | Directory layout, zero-import architecture, file suffixes, `spectra.config.ts`, core CLI commands |
|
|
56
|
+
| `spec-and-suite-authoring` | `suite.md` / `spec.md` frontmatter, the paired `*.test.ts` script, every `spectra lint` rule |
|
|
57
|
+
| `page-objects-and-selectors` | Page Object pattern (`Spectra.get`/`getAll`), the `~`/`#` selector contract |
|
|
58
|
+
| `matchers-and-assertions` | Full element action/assertion/collection matcher and global `Spectra.*` command reference |
|
|
59
|
+
| `shared-steps-and-actions` | `Step.*` business workflows vs `support/actions/*` technical utilities |
|
|
60
|
+
| `lifecycle-hooks` | `global-hooks/` vs `specs/<suite>/hooks/`, execution order, failure cascade |
|
|
61
|
+
| `fixtures-data` | `fixtures/*.json` and the ambient `Fixture.*` namespace |
|
|
62
|
+
| `network-interception` | `Spectra.intercept()` mocking, polling, and failure-simulation patterns |
|
|
63
|
+
|
|
64
|
+
Each skill is grounded in `docs/v2/` and the reference workspace at `cli/templates/default`, so an
|
|
65
|
+
AI coding agent scaffolding TestSpectra scripts produces code that already passes `spectra lint`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Multi-Agent, Cross-Platform**: Installs into `.claude/skills`, `.agents/skills`,
|
|
72
|
+
`.opencode/skills`, `.kilo/skills`, and the Gemini CLI's global skills directory. `init` lets you
|
|
73
|
+
pick explicitly; otherwise it auto-detects whichever the target project/agent already uses.
|
|
74
|
+
- **Independent Usage**: Works in any TestSpectra workspace (default or Nx/monorepo template)
|
|
75
|
+
without requiring the main `@testspectra/cli` to be installed first.
|
|
76
|
+
- **Update-in-Place**: `update` re-installs every currently-installed skill from the package's
|
|
77
|
+
current registry, so bumping `@testspectra/skills` and re-running `update` refreshes local
|
|
78
|
+
copies.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function addSkills(skillIds: string[], baseDir?: string, agentIds?: string[]): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import { intro, outro, multiselect, isCancel, cancel } from '@clack/prompts';
|
|
3
|
+
import { installSkill } from '../detector.js';
|
|
4
|
+
import { loadAllSkills, getSkill } from '../registry.js';
|
|
5
|
+
export async function addSkills(skillIds, baseDir = process.cwd(), agentIds) {
|
|
6
|
+
intro('\x1b[1m\x1b[36m➕ Install TestSpectra Script-Authoring Skills\x1b[0m');
|
|
7
|
+
const allSkills = loadAllSkills();
|
|
8
|
+
let targetIds = skillIds.filter(Boolean);
|
|
9
|
+
if (targetIds.length === 0) {
|
|
10
|
+
const selected = await multiselect({
|
|
11
|
+
message: 'Select TestSpectra skills to install into your AI agent environment:',
|
|
12
|
+
options: allSkills.map((s) => ({
|
|
13
|
+
value: s.id,
|
|
14
|
+
label: s.id,
|
|
15
|
+
hint: s.description,
|
|
16
|
+
})),
|
|
17
|
+
required: true,
|
|
18
|
+
});
|
|
19
|
+
if (isCancel(selected)) {
|
|
20
|
+
cancel('Operation cancelled.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
targetIds = selected;
|
|
24
|
+
}
|
|
25
|
+
const installedList = [];
|
|
26
|
+
for (const id of targetIds) {
|
|
27
|
+
const skill = getSkill(id);
|
|
28
|
+
if (!skill) {
|
|
29
|
+
console.log(`\x1b[33m⚠️ Skill "${id}" not found in registry. Run "list" to view available skills.\x1b[0m`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const paths = installSkill(skill, baseDir, agentIds);
|
|
33
|
+
installedList.push(skill.id);
|
|
34
|
+
for (const p of paths) {
|
|
35
|
+
console.log(` \x1b[32m✔\x1b[0m Installed \x1b[1m${skill.id}\x1b[0m -> \x1b[2m${p}\x1b[0m`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (installedList.length > 0) {
|
|
39
|
+
outro(`\x1b[32mSuccessfully installed ${installedList.length} skill(s)!\x1b[0m`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
outro('No skills were installed.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initSkills(baseDir?: string): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import { intro, outro, multiselect, isCancel, cancel } from '@clack/prompts';
|
|
3
|
+
import { KNOWN_AGENTS, writeAgentConfig, installSkill } from '../detector.js';
|
|
4
|
+
import { loadAllSkills, getSkill } from '../registry.js';
|
|
5
|
+
export async function initSkills(baseDir = process.cwd()) {
|
|
6
|
+
intro('\x1b[1m\x1b[36m🧭 TestSpectra Skills Setup\x1b[0m');
|
|
7
|
+
const selectedAgents = await multiselect({
|
|
8
|
+
message: 'Which AI agent(s) do you use in this project? (saved for future add/update/remove/list)',
|
|
9
|
+
options: KNOWN_AGENTS.map((a) => ({
|
|
10
|
+
value: a.id,
|
|
11
|
+
label: a.label,
|
|
12
|
+
})),
|
|
13
|
+
required: true,
|
|
14
|
+
});
|
|
15
|
+
if (isCancel(selectedAgents)) {
|
|
16
|
+
cancel('Operation cancelled.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const agentIds = selectedAgents;
|
|
20
|
+
const configPath = writeAgentConfig(baseDir, agentIds);
|
|
21
|
+
console.log(` \x1b[32m✔\x1b[0m Saved agent selection (\x1b[1m${agentIds.join(', ')}\x1b[0m) -> \x1b[2m${configPath}\x1b[0m`);
|
|
22
|
+
const allSkills = loadAllSkills();
|
|
23
|
+
const selectedSkills = await multiselect({
|
|
24
|
+
message: 'Which skills do you want to install now for those agent(s)?',
|
|
25
|
+
options: allSkills.map((s) => ({
|
|
26
|
+
value: s.id,
|
|
27
|
+
label: s.id,
|
|
28
|
+
hint: s.description,
|
|
29
|
+
})),
|
|
30
|
+
initialValues: allSkills.map((s) => s.id),
|
|
31
|
+
required: false,
|
|
32
|
+
});
|
|
33
|
+
if (isCancel(selectedSkills)) {
|
|
34
|
+
outro('Agent selection saved. No skills installed — run `add` anytime to install skills.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const skillIds = selectedSkills;
|
|
38
|
+
if (skillIds.length === 0) {
|
|
39
|
+
outro('Agent selection saved. No skills installed — run `add` anytime to install skills.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const installedList = [];
|
|
43
|
+
for (const id of skillIds) {
|
|
44
|
+
const skill = getSkill(id);
|
|
45
|
+
if (!skill)
|
|
46
|
+
continue;
|
|
47
|
+
const paths = installSkill(skill, baseDir, agentIds);
|
|
48
|
+
installedList.push(skill.id);
|
|
49
|
+
for (const p of paths) {
|
|
50
|
+
console.log(` \x1b[32m✔\x1b[0m Installed \x1b[1m${skill.id}\x1b[0m -> \x1b[2m${p}\x1b[0m`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
outro(`\x1b[32mSetup complete!\x1b[0m Installed ${installedList.length} skill(s) for agent(s): ${agentIds.join(', ')}.`);
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listSkills(baseDir?: string, agentIds?: string[]): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import { intro, outro } from '@clack/prompts';
|
|
3
|
+
import { getAgentSkillDirs, isSkillInstalled } from '../detector.js';
|
|
4
|
+
import { loadAllSkills } from '../registry.js';
|
|
5
|
+
export function listSkills(baseDir = process.cwd(), agentIds) {
|
|
6
|
+
intro('\x1b[1m\x1b[36m📦 TestSpectra Skills Registry\x1b[0m');
|
|
7
|
+
const allSkills = loadAllSkills();
|
|
8
|
+
const targetDirs = getAgentSkillDirs(baseDir, agentIds);
|
|
9
|
+
if (allSkills.length === 0) {
|
|
10
|
+
console.log('No skills found in registry.');
|
|
11
|
+
outro('Done.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(`\nAgent directories: \x1b[2m${targetDirs.join(', ')}\x1b[0m\n`);
|
|
15
|
+
for (const skill of allSkills) {
|
|
16
|
+
const installed = isSkillInstalled(skill.id, baseDir, agentIds);
|
|
17
|
+
const statusBadge = installed ? '\x1b[32m[Installed]\x1b[0m' : '\x1b[90m[Available]\x1b[0m';
|
|
18
|
+
console.log(` ${statusBadge} \x1b[1m${skill.id}\x1b[0m`);
|
|
19
|
+
console.log(` \x1b[90m${skill.description}\x1b[0m\n`);
|
|
20
|
+
}
|
|
21
|
+
outro(`Total skills: ${allSkills.length} | Run \x1b[36mnpx @testspectra/skills add <skill-name>\x1b[0m to install.`);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function removeSkills(skillIds: string[], baseDir?: string, agentIds?: string[]): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import { intro, outro } from '@clack/prompts';
|
|
3
|
+
import { removeSkill } from '../detector.js';
|
|
4
|
+
export function removeSkills(skillIds, baseDir = process.cwd(), agentIds) {
|
|
5
|
+
intro('\x1b[1m\x1b[31m🗑️ Remove TestSpectra Skills\x1b[0m');
|
|
6
|
+
const targets = skillIds.filter(Boolean);
|
|
7
|
+
if (targets.length === 0) {
|
|
8
|
+
console.log('Please specify skill name(s) to remove.');
|
|
9
|
+
outro('Aborted.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const removedList = [];
|
|
13
|
+
for (const id of targets) {
|
|
14
|
+
const paths = removeSkill(id, baseDir, agentIds);
|
|
15
|
+
if (paths.length > 0) {
|
|
16
|
+
removedList.push(id);
|
|
17
|
+
for (const p of paths) {
|
|
18
|
+
console.log(` \x1b[31m✔\x1b[0m Removed \x1b[1m${id}\x1b[0m from \x1b[2m${p}\x1b[0m`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(` \x1b[90m- Skill "${id}" was not installed.\x1b[0m`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
outro(`\x1b[32mCompleted. Removed ${removedList.length} skill(s).\x1b[0m`);
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateSkills(skillIds?: string[], baseDir?: string, agentIds?: string[]): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import { intro, outro } from '@clack/prompts';
|
|
3
|
+
import { getInstalledSkills, installSkill } from '../detector.js';
|
|
4
|
+
import { getSkill } from '../registry.js';
|
|
5
|
+
export function updateSkills(skillIds = [], baseDir = process.cwd(), agentIds) {
|
|
6
|
+
intro('\x1b[1m\x1b[36m🔄 Update TestSpectra Skills\x1b[0m');
|
|
7
|
+
let targets = skillIds.filter(Boolean);
|
|
8
|
+
if (targets.length === 0) {
|
|
9
|
+
targets = getInstalledSkills(baseDir, agentIds);
|
|
10
|
+
}
|
|
11
|
+
if (targets.length === 0) {
|
|
12
|
+
console.log('No installed skills found in project to update.');
|
|
13
|
+
outro('Done.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const updatedList = [];
|
|
17
|
+
for (const id of targets) {
|
|
18
|
+
const skill = getSkill(id);
|
|
19
|
+
if (!skill) {
|
|
20
|
+
console.log(`\x1b[33m⚠️ Skill "${id}" not found in current registry.\x1b[0m`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const paths = installSkill(skill, baseDir, agentIds);
|
|
24
|
+
updatedList.push(skill.id);
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
console.log(` \x1b[32m✔\x1b[0m Updated \x1b[1m${skill.id}\x1b[0m -> \x1b[2m${p}\x1b[0m`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
outro(`\x1b[32mSuccessfully updated ${updatedList.length} skill(s)!\x1b[0m`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SkillItem } from './registry.js';
|
|
2
|
+
export interface AgentDefinition {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
resolve: (baseDir: string) => string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Every AI agent / coding assistant this package knows how to target.
|
|
9
|
+
* `resolve()` returns the absolute skills directory for that agent given a workspace baseDir.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KNOWN_AGENTS: AgentDefinition[];
|
|
12
|
+
export declare function getKnownAgentIds(): string[];
|
|
13
|
+
/** Reads the agent selection persisted by `spectra-skills init`, if any. */
|
|
14
|
+
export declare function readAgentConfig(baseDir?: string): string[] | undefined;
|
|
15
|
+
/** Persists the agent selection chosen via `spectra-skills init` for future add/update/remove/list calls. */
|
|
16
|
+
export declare function writeAgentConfig(baseDir: string, agentIds: string[]): string;
|
|
17
|
+
/** Splits a comma/whitespace separated --agent value and reports any id not in KNOWN_AGENTS. */
|
|
18
|
+
export declare function resolveAgentSelection(raw?: string): {
|
|
19
|
+
ids: string[];
|
|
20
|
+
unknown: string[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolves target skill directories, in priority order:
|
|
24
|
+
*
|
|
25
|
+
* 1. `agentIds` passed explicitly (e.g. a one-off `--agent` override): resolves exactly those
|
|
26
|
+
* agents' directories, creating them even if the agent isn't already set up in this workspace.
|
|
27
|
+
* 2. Agent selection persisted by `spectra-skills init` (`.spectra-skills.json`), if present.
|
|
28
|
+
* 3. Auto-detect: only targets agents whose directory (or its parent, e.g. `.claude`) already
|
|
29
|
+
* exists, falling back to the universal `.agents/skills` root when nothing is detected.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAgentSkillDirs(baseDir?: string, agentIds?: string[]): string[];
|
|
32
|
+
export declare function isSkillInstalled(skillId: string, baseDir?: string, agentIds?: string[]): boolean;
|
|
33
|
+
export declare function getInstalledSkills(baseDir?: string, agentIds?: string[]): string[];
|
|
34
|
+
export declare function installSkill(skill: SkillItem, baseDir?: string, agentIds?: string[]): string[];
|
|
35
|
+
export declare function removeSkill(skillId: string, baseDir?: string, agentIds?: string[]): string[];
|
package/dist/detector.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Every AI agent / coding assistant this package knows how to target.
|
|
6
|
+
* `resolve()` returns the absolute skills directory for that agent given a workspace baseDir.
|
|
7
|
+
*/
|
|
8
|
+
export const KNOWN_AGENTS = [
|
|
9
|
+
{ id: 'agents', label: 'Universal (.agents/skills)', resolve: (b) => path.join(b, '.agents', 'skills') },
|
|
10
|
+
{ id: 'claude', label: 'Claude Code (.claude/skills)', resolve: (b) => path.join(b, '.claude', 'skills') },
|
|
11
|
+
{ id: 'opencode', label: 'OpenCode (.opencode/skills)', resolve: (b) => path.join(b, '.opencode', 'skills') },
|
|
12
|
+
{ id: 'kilo', label: 'Kilo (.kilo/skills)', resolve: (b) => path.join(b, '.kilo', 'skills') },
|
|
13
|
+
{
|
|
14
|
+
id: 'gemini',
|
|
15
|
+
label: 'Gemini CLI (~/.gemini/config/skills, global)',
|
|
16
|
+
resolve: () => path.join(os.homedir(), '.gemini', 'config', 'skills'),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
export function getKnownAgentIds() {
|
|
20
|
+
return KNOWN_AGENTS.map((a) => a.id);
|
|
21
|
+
}
|
|
22
|
+
const CONFIG_FILENAME = '.spectra-skills.json';
|
|
23
|
+
/** Reads the agent selection persisted by `spectra-skills init`, if any. */
|
|
24
|
+
export function readAgentConfig(baseDir = process.cwd()) {
|
|
25
|
+
const configPath = path.join(baseDir, CONFIG_FILENAME);
|
|
26
|
+
if (!fs.existsSync(configPath))
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
30
|
+
const known = new Set(getKnownAgentIds());
|
|
31
|
+
const agents = (parsed.agents ?? []).filter((id) => known.has(id));
|
|
32
|
+
return agents.length > 0 ? agents : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Persists the agent selection chosen via `spectra-skills init` for future add/update/remove/list calls. */
|
|
39
|
+
export function writeAgentConfig(baseDir, agentIds) {
|
|
40
|
+
const configPath = path.join(baseDir, CONFIG_FILENAME);
|
|
41
|
+
fs.writeFileSync(configPath, JSON.stringify({ agents: agentIds }, null, 2) + '\n', 'utf-8');
|
|
42
|
+
return configPath;
|
|
43
|
+
}
|
|
44
|
+
/** Splits a comma/whitespace separated --agent value and reports any id not in KNOWN_AGENTS. */
|
|
45
|
+
export function resolveAgentSelection(raw) {
|
|
46
|
+
if (!raw)
|
|
47
|
+
return { ids: [], unknown: [] };
|
|
48
|
+
const requested = raw
|
|
49
|
+
.split(/[,\s]+/)
|
|
50
|
+
.map((s) => s.trim().toLowerCase())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
const known = new Set(getKnownAgentIds());
|
|
53
|
+
const ids = requested.filter((id) => known.has(id));
|
|
54
|
+
const unknown = requested.filter((id) => !known.has(id));
|
|
55
|
+
return { ids, unknown };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves target skill directories, in priority order:
|
|
59
|
+
*
|
|
60
|
+
* 1. `agentIds` passed explicitly (e.g. a one-off `--agent` override): resolves exactly those
|
|
61
|
+
* agents' directories, creating them even if the agent isn't already set up in this workspace.
|
|
62
|
+
* 2. Agent selection persisted by `spectra-skills init` (`.spectra-skills.json`), if present.
|
|
63
|
+
* 3. Auto-detect: only targets agents whose directory (or its parent, e.g. `.claude`) already
|
|
64
|
+
* exists, falling back to the universal `.agents/skills` root when nothing is detected.
|
|
65
|
+
*/
|
|
66
|
+
export function getAgentSkillDirs(baseDir = process.cwd(), agentIds) {
|
|
67
|
+
const explicitOrConfigured = agentIds && agentIds.length > 0 ? agentIds : readAgentConfig(baseDir);
|
|
68
|
+
if (explicitOrConfigured && explicitOrConfigured.length > 0) {
|
|
69
|
+
const known = new Map(KNOWN_AGENTS.map((a) => [a.id, a]));
|
|
70
|
+
return explicitOrConfigured
|
|
71
|
+
.map((id) => known.get(id.toLowerCase()))
|
|
72
|
+
.filter((a) => Boolean(a))
|
|
73
|
+
.map((a) => a.resolve(baseDir));
|
|
74
|
+
}
|
|
75
|
+
const dirs = [];
|
|
76
|
+
for (const agent of KNOWN_AGENTS) {
|
|
77
|
+
const dir = agent.resolve(baseDir);
|
|
78
|
+
if (fs.existsSync(dir) || fs.existsSync(path.dirname(dir))) {
|
|
79
|
+
dirs.push(dir);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// No agent folder detected yet -> default to the universal agent root.
|
|
83
|
+
if (dirs.length === 0) {
|
|
84
|
+
dirs.push(path.join(baseDir, '.agents', 'skills'));
|
|
85
|
+
}
|
|
86
|
+
return dirs;
|
|
87
|
+
}
|
|
88
|
+
export function isSkillInstalled(skillId, baseDir = process.cwd(), agentIds) {
|
|
89
|
+
const dirs = getAgentSkillDirs(baseDir, agentIds);
|
|
90
|
+
return dirs.some((d) => fs.existsSync(path.join(d, skillId, 'SKILL.md')));
|
|
91
|
+
}
|
|
92
|
+
export function getInstalledSkills(baseDir = process.cwd(), agentIds) {
|
|
93
|
+
const dirs = getAgentSkillDirs(baseDir, agentIds);
|
|
94
|
+
const installed = new Set();
|
|
95
|
+
for (const d of dirs) {
|
|
96
|
+
if (!fs.existsSync(d))
|
|
97
|
+
continue;
|
|
98
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
99
|
+
if (entry.isDirectory() && fs.existsSync(path.join(d, entry.name, 'SKILL.md'))) {
|
|
100
|
+
installed.add(entry.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Array.from(installed);
|
|
105
|
+
}
|
|
106
|
+
export function installSkill(skill, baseDir = process.cwd(), agentIds) {
|
|
107
|
+
const targetDirs = getAgentSkillDirs(baseDir, agentIds);
|
|
108
|
+
const writtenPaths = [];
|
|
109
|
+
for (const baseSkillDir of targetDirs) {
|
|
110
|
+
const destDir = path.join(baseSkillDir, skill.id);
|
|
111
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
112
|
+
const destFile = path.join(destDir, 'SKILL.md');
|
|
113
|
+
fs.writeFileSync(destFile, skill.content, 'utf-8');
|
|
114
|
+
writtenPaths.push(destFile);
|
|
115
|
+
}
|
|
116
|
+
return writtenPaths;
|
|
117
|
+
}
|
|
118
|
+
export function removeSkill(skillId, baseDir = process.cwd(), agentIds) {
|
|
119
|
+
const targetDirs = getAgentSkillDirs(baseDir, agentIds);
|
|
120
|
+
const removedPaths = [];
|
|
121
|
+
for (const baseSkillDir of targetDirs) {
|
|
122
|
+
const destDir = path.join(baseSkillDir, skillId);
|
|
123
|
+
if (fs.existsSync(destDir)) {
|
|
124
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
125
|
+
removedPaths.push(destDir);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return removedPaths;
|
|
129
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './registry.js';
|
|
2
|
+
export * from './detector.js';
|
|
3
|
+
export * from './commands/init.js';
|
|
4
|
+
export * from './commands/list.js';
|
|
5
|
+
export * from './commands/add.js';
|
|
6
|
+
export * from './commands/update.js';
|
|
7
|
+
export * from './commands/remove.js';
|
|
8
|
+
export declare function runCli(): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { initSkills } from './commands/init.js';
|
|
7
|
+
import { listSkills } from './commands/list.js';
|
|
8
|
+
import { addSkills } from './commands/add.js';
|
|
9
|
+
import { updateSkills } from './commands/update.js';
|
|
10
|
+
import { removeSkills } from './commands/remove.js';
|
|
11
|
+
import { getKnownAgentIds, resolveAgentSelection } from './detector.js';
|
|
12
|
+
export * from './registry.js';
|
|
13
|
+
export * from './detector.js';
|
|
14
|
+
export * from './commands/init.js';
|
|
15
|
+
export * from './commands/list.js';
|
|
16
|
+
export * from './commands/add.js';
|
|
17
|
+
export * from './commands/update.js';
|
|
18
|
+
export * from './commands/remove.js';
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
function getPackageVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
return pkg.version ?? '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return '0.0.0';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const HELP_TEXT = `
|
|
31
|
+
\x1b[1m\x1b[36m🚀 TestSpectra Skills CLI\x1b[0m
|
|
32
|
+
Install, update, and manage modular AI agent skills that standardize TestSpectra script authoring.
|
|
33
|
+
|
|
34
|
+
\x1b[1mUsage:\x1b[0m
|
|
35
|
+
npx @testspectra/skills [command] [options]
|
|
36
|
+
spectra-skills [command] [options]
|
|
37
|
+
|
|
38
|
+
\x1b[1mCommands:\x1b[0m
|
|
39
|
+
\x1b[36minit\x1b[0m Prompt which AI agent(s) you use and save the choice for future commands
|
|
40
|
+
\x1b[36mlist\x1b[0m List all available and installed skills
|
|
41
|
+
\x1b[36madd\x1b[0m [skills...] Install specified skills (interactive if none provided)
|
|
42
|
+
\x1b[36mupdate\x1b[0m [skills...] Update installed skills to latest versions
|
|
43
|
+
\x1b[36mremove\x1b[0m <skills...> Remove specified skills from agent environment
|
|
44
|
+
|
|
45
|
+
\x1b[1mOptions:\x1b[0m
|
|
46
|
+
--target <dir> Target directory (default: current working directory)
|
|
47
|
+
--agent <ids> One-off override: comma-separated agent(s) to target, bypassing the
|
|
48
|
+
saved "init" selection and auto-detection. Known ids: ${getKnownAgentIds().join(', ')}
|
|
49
|
+
-h, --help Show help message
|
|
50
|
+
-v, --version Show version
|
|
51
|
+
|
|
52
|
+
\x1b[1mExamples:\x1b[0m
|
|
53
|
+
npx @testspectra/skills init
|
|
54
|
+
npx @testspectra/skills list
|
|
55
|
+
npx @testspectra/skills add workspace-structure
|
|
56
|
+
npx @testspectra/skills add spec-and-suite-authoring matchers-and-assertions
|
|
57
|
+
npx @testspectra/skills update
|
|
58
|
+
npx @testspectra/skills remove network-interception
|
|
59
|
+
`;
|
|
60
|
+
export async function runCli() {
|
|
61
|
+
const args = process.argv.slice(2);
|
|
62
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
63
|
+
console.log(HELP_TEXT);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
67
|
+
console.log(getPackageVersion());
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const parsed = parseArgs({
|
|
71
|
+
args,
|
|
72
|
+
options: {
|
|
73
|
+
target: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
},
|
|
76
|
+
agent: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
allowPositionals: true,
|
|
81
|
+
strict: false,
|
|
82
|
+
});
|
|
83
|
+
const baseDir = typeof parsed.values.target === 'string' ? path.resolve(parsed.values.target) : process.cwd();
|
|
84
|
+
const rawAgent = typeof parsed.values.agent === 'string' ? parsed.values.agent : undefined;
|
|
85
|
+
const { ids: agentIds, unknown: unknownAgents } = resolveAgentSelection(rawAgent);
|
|
86
|
+
if (unknownAgents.length > 0) {
|
|
87
|
+
console.log(`\x1b[33m⚠️ Unknown --agent value(s): ${unknownAgents.join(', ')}. Known ids: ${getKnownAgentIds().join(', ')}\x1b[0m`);
|
|
88
|
+
}
|
|
89
|
+
const { positionals } = parsed;
|
|
90
|
+
const command = positionals[0] || 'list';
|
|
91
|
+
const skillArgs = positionals.slice(1);
|
|
92
|
+
switch (command) {
|
|
93
|
+
case 'init':
|
|
94
|
+
await initSkills(baseDir);
|
|
95
|
+
break;
|
|
96
|
+
case 'list':
|
|
97
|
+
case 'ls':
|
|
98
|
+
listSkills(baseDir, agentIds);
|
|
99
|
+
break;
|
|
100
|
+
case 'add':
|
|
101
|
+
case 'install':
|
|
102
|
+
case 'i':
|
|
103
|
+
await addSkills(skillArgs, baseDir, agentIds);
|
|
104
|
+
break;
|
|
105
|
+
case 'update':
|
|
106
|
+
case 'up':
|
|
107
|
+
updateSkills(skillArgs, baseDir, agentIds);
|
|
108
|
+
break;
|
|
109
|
+
case 'remove':
|
|
110
|
+
case 'rm':
|
|
111
|
+
case 'delete':
|
|
112
|
+
removeSkills(skillArgs, baseDir, agentIds);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
console.log(`\x1b[31mUnknown command: "${command}"\x1b[0m`);
|
|
116
|
+
console.log(HELP_TEXT);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|