@rfxlamia/skillkit 1.0.0 → 1.1.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.
Files changed (25) hide show
  1. package/package.json +7 -2
  2. package/skills/quick-spec/tests/__pycache__/test_skill.cpython-314-pytest-9.0.2.pyc +0 -0
  3. package/skills/skillkit/.claude/settings.local.json +7 -0
  4. package/skills/skillkit/scripts/__pycache__/decision_helper.cpython-314.pyc +0 -0
  5. package/skills/skillkit/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
  6. package/skills/skillkit/scripts/__pycache__/quick_validate.cpython-314.pyc +0 -0
  7. package/skills/skillkit/scripts/__pycache__/test_generator.cpython-314-pytest-9.0.2.pyc +0 -0
  8. package/skills/skillkit/scripts/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  9. package/skills/skillkit/scripts/utils/__pycache__/__init__.cpython-314.pyc +0 -0
  10. package/skills/skillkit/scripts/utils/__pycache__/budget_tracker.cpython-312.pyc +0 -0
  11. package/skills/skillkit/scripts/utils/__pycache__/budget_tracker.cpython-314.pyc +0 -0
  12. package/skills/skillkit/scripts/utils/__pycache__/output_formatter.cpython-312.pyc +0 -0
  13. package/skills/skillkit/scripts/utils/__pycache__/output_formatter.cpython-314.pyc +0 -0
  14. package/skills/skillkit/scripts/utils/__pycache__/reference_validator.cpython-312.pyc +0 -0
  15. package/skills/skillkit/scripts/utils/__pycache__/reference_validator.cpython-314.pyc +0 -0
  16. package/skills-manifest.json +1 -1
  17. package/src/banner.js +1 -1
  18. package/src/cli.js +15 -4
  19. package/src/install.js +45 -29
  20. package/src/install.test.js +75 -7
  21. package/src/picker.js +9 -4
  22. package/src/scope.js +8 -39
  23. package/src/scope.test.js +9 -13
  24. package/src/tools.js +76 -0
  25. package/src/tools.test.js +80 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rfxlamia/skillkit",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Interactive CLI installer for SkillKit — Claude Code skills & agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,5 +31,10 @@
31
31
  },
32
32
  "homepage": "https://github.com/rfxlamia/skillkit",
33
33
  "license": "Apache-2.0",
34
- "keywords": ["claude-code", "skills", "ai-agent", "skillkit"]
34
+ "keywords": [
35
+ "claude-code",
36
+ "skills",
37
+ "ai-agent",
38
+ "skillkit"
39
+ ]
35
40
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__tavily__tavily-search"
5
+ ]
6
+ }
7
+ }
@@ -213,5 +213,5 @@
213
213
  "path": "agents/seo-manager.md"
214
214
  }
215
215
  ],
216
- "generatedAt": "2026-03-14T18:10:24.737Z"
216
+ "generatedAt": "2026-03-15T04:29:19.063Z"
217
217
  }
package/src/banner.js CHANGED
@@ -5,6 +5,6 @@ export function printBanner(version) {
5
5
  ███▄▄ ██▄█▀ ██ ██ ██ ██▄█▀ ██ ██
6
6
  ▄▄██▀ ██ ██ ██ ██▄▄▄ ██▄▄▄ ██ ██ ██ ██
7
7
  \x1b[0m
8
- \x1b[90mv${version} · Claude Code Skills Installer\x1b[0m
8
+ \x1b[90mv${version} · Multi-Tool Skills Installer\x1b[0m
9
9
  `)
10
10
  }
package/src/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ // installer/src/cli.js
1
2
  import { intro, outro, cancel, isCancel, log } from '@clack/prompts'
2
3
  import { readFileSync } from 'fs'
3
4
  import { join, dirname } from 'path'
@@ -7,6 +8,7 @@ import { selectScope } from './scope.js'
7
8
  import { pickInstallables } from './picker.js'
8
9
  import { installSelected } from './install.js'
9
10
  import { checkForUpdates } from './update.js'
11
+ import { selectTools, getToolTargets } from './tools.js'
10
12
 
11
13
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
14
  const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
@@ -17,14 +19,23 @@ export async function run() {
17
19
 
18
20
  intro('SkillKit Installer')
19
21
 
20
- const scope = await selectScope()
22
+ const selectedTools = await selectTools()
23
+ if (isCancel(selectedTools)) { cancel('Cancelled.'); process.exit(0) }
24
+
25
+ const scope = await selectScope(selectedTools)
21
26
  if (isCancel(scope)) { cancel('Cancelled.'); process.exit(0) }
22
27
 
23
28
  const selected = await pickInstallables()
24
29
  if (isCancel(selected)) { cancel('Cancelled.'); process.exit(0) }
25
30
 
26
- const { installed } = await installSelected(selected, scope)
31
+ const targets = getToolTargets(selectedTools, scope)
32
+ const { results, totalInstalled } = await installSelected(selected, targets)
33
+
34
+ const targetLabels = results
35
+ .filter(r => r.installed > 0)
36
+ .map(r => `${r.target.name} (${r.target.scope})`)
37
+ .join(', ')
27
38
 
28
- outro(`Done! ${installed} item(s) installed to ${scope.scope} scope.`)
29
- log.info(`Restart Claude Code to pick up new skills.`)
39
+ outro(`Done! ${totalInstalled} item(s) installed to ${targetLabels || 'no targets'}.`)
40
+ log.info('Restart your coding agent tools to pick up new skills.')
30
41
  }
package/src/install.js CHANGED
@@ -1,3 +1,4 @@
1
+ // installer/src/install.js
1
2
  import { cpSync, mkdirSync, existsSync } from 'fs'
2
3
  import { join, dirname } from 'path'
3
4
  import { fileURLToPath } from 'url'
@@ -6,48 +7,63 @@ import { spinner, log } from '@clack/prompts'
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
7
8
  const PACKAGE_ROOT = join(__dirname, '..')
8
9
 
9
- export async function installSelected({ skills, agents }, { skillsDir, agentsDir }) {
10
- // Validate inputs
10
+ export async function installSelected({ skills, agents }, targets) {
11
11
  if (!Array.isArray(skills)) throw new TypeError('skills must be an array')
12
12
  if (!Array.isArray(agents)) throw new TypeError('agents must be an array')
13
+ if (!Array.isArray(targets)) throw new TypeError('targets must be an array')
13
14
 
14
15
  const s = spinner()
15
16
  s.start('Installing...')
16
17
 
17
- let installed = 0
18
- const skipped = []
18
+ const results = []
19
+ let totalInstalled = 0
19
20
 
20
- for (const skill of skills) {
21
- if (!skill.name || !skill.path) {
22
- skipped.push(`invalid-skill-${installed}`)
23
- continue
21
+ for (const target of targets) {
22
+ let installed = 0
23
+ const skipped = []
24
+
25
+ if (target.skillsDir) {
26
+ for (const skill of skills) {
27
+ if (!skill.name || !skill.path) {
28
+ skipped.push(`invalid-skill-${skipped.length}`)
29
+ continue
30
+ }
31
+ const src = join(PACKAGE_ROOT, skill.path)
32
+ const dest = join(target.skillsDir, skill.name)
33
+ if (!existsSync(src)) { skipped.push(skill.name); continue }
34
+ mkdirSync(dest, { recursive: true })
35
+ cpSync(src, dest, { recursive: true })
36
+ installed++
37
+ }
24
38
  }
25
- const src = join(PACKAGE_ROOT, skill.path)
26
- const dest = join(skillsDir, skill.name)
27
- if (!existsSync(src)) { skipped.push(skill.name); continue }
28
- mkdirSync(dest, { recursive: true })
29
- cpSync(src, dest, { recursive: true })
30
- installed++
31
- }
32
39
 
33
- for (const agent of agents) {
34
- if (!agent.name || !agent.path) {
35
- skipped.push(`invalid-agent-${installed}`)
36
- continue
40
+ if (target.agentsDir) {
41
+ for (const agent of agents) {
42
+ if (!agent.name || !agent.path) {
43
+ skipped.push(`invalid-agent-${skipped.length}`)
44
+ continue
45
+ }
46
+ const src = join(PACKAGE_ROOT, agent.path)
47
+ const dest = join(target.agentsDir, agent.name + '.md')
48
+ if (!existsSync(src)) { skipped.push(agent.name); continue }
49
+ mkdirSync(target.agentsDir, { recursive: true })
50
+ cpSync(src, dest)
51
+ installed++
52
+ }
53
+ } else if (agents.length > 0) {
54
+ log.warn(`Agents are not supported for ${target.name} — skipped.`)
37
55
  }
38
- const src = join(PACKAGE_ROOT, agent.path)
39
- const dest = join(agentsDir, agent.name + '.md')
40
- if (!existsSync(src)) { skipped.push(agent.name); continue }
41
- mkdirSync(agentsDir, { recursive: true })
42
- cpSync(src, dest)
43
- installed++
56
+
57
+ results.push({ target, installed, skipped })
58
+ totalInstalled += installed
44
59
  }
45
60
 
46
- s.stop(`Installed ${installed} item(s)`)
61
+ s.stop(`Installed ${totalInstalled} item(s)`)
47
62
 
48
- if (skipped.length > 0) {
49
- log.warn(`Skipped (not found in package): ${skipped.join(', ')}`)
63
+ const allSkipped = results.flatMap(r => r.skipped)
64
+ if (allSkipped.length > 0) {
65
+ log.warn(`Skipped (not found in package): ${allSkipped.join(', ')}`)
50
66
  }
51
67
 
52
- return { installed, skipped }
68
+ return { results, totalInstalled }
53
69
  }
@@ -1,40 +1,108 @@
1
+ // installer/src/install.test.js
1
2
  import { test } from 'node:test'
2
3
  import assert from 'node:assert'
3
4
  import { mkdtempSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs'
4
- import { join } from 'path'
5
+ import { join, dirname } from 'path'
5
6
  import { tmpdir } from 'os'
7
+ import { fileURLToPath } from 'url'
6
8
  import { installSelected } from './install.js'
7
9
 
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const PACKAGE_ROOT = join(__dirname, '..')
12
+
8
13
  function createTempDir() {
9
14
  return mkdtempSync(join(tmpdir(), 'skillkit-test-'))
10
15
  }
11
16
 
17
+ function makeTarget(skillsDir, agentsDir, name = 'Test Tool') {
18
+ return { name, scope: 'user', skillsDir, agentsDir }
19
+ }
20
+
12
21
  test('installSelected validates skills is an array', async () => {
13
22
  await assert.rejects(
14
- installSelected({ skills: 'not-array', agents: [] }, { skillsDir: '/tmp', agentsDir: '/tmp' }),
23
+ installSelected({ skills: 'not-array', agents: [] }, [makeTarget('/tmp', '/tmp')]),
15
24
  /skills must be an array/
16
25
  )
17
26
  })
18
27
 
19
28
  test('installSelected validates agents is an array', async () => {
20
29
  await assert.rejects(
21
- installSelected({ skills: [], agents: 'not-array' }, { skillsDir: '/tmp', agentsDir: '/tmp' }),
30
+ installSelected({ skills: [], agents: 'not-array' }, [makeTarget('/tmp', '/tmp')]),
22
31
  /agents must be an array/
23
32
  )
24
33
  })
25
34
 
35
+ test('installSelected validates targets is an array', async () => {
36
+ await assert.rejects(
37
+ installSelected({ skills: [], agents: [] }, 'not-array'),
38
+ /targets must be an array/
39
+ )
40
+ })
41
+
26
42
  test('installSelected skips invalid skill objects', async () => {
27
43
  const skillsDir = createTempDir()
28
44
  const agentsDir = createTempDir()
29
45
 
30
- const result = await installSelected(
46
+ const { totalInstalled, results } = await installSelected(
31
47
  { skills: [{ name: null }, { name: 'valid', path: 'skills/valid' }], agents: [] },
32
- { skillsDir, agentsDir }
48
+ [makeTarget(skillsDir, agentsDir)]
33
49
  )
34
50
 
35
- assert.strictEqual(result.installed, 0)
36
- assert.strictEqual(result.skipped.length, 2)
51
+ assert.strictEqual(totalInstalled, 0)
52
+ assert.strictEqual(results[0].skipped.length, 2)
37
53
 
38
54
  rmSync(skillsDir, { recursive: true })
39
55
  rmSync(agentsDir, { recursive: true })
40
56
  })
57
+
58
+ test('installSelected returns results per target', async () => {
59
+ const dir1 = createTempDir()
60
+ const dir2 = createTempDir()
61
+
62
+ const { results, totalInstalled } = await installSelected(
63
+ { skills: [], agents: [] },
64
+ [makeTarget(dir1, null, 'Tool A'), makeTarget(dir2, null, 'Tool B')]
65
+ )
66
+
67
+ assert.strictEqual(results.length, 2)
68
+ assert.strictEqual(results[0].target.name, 'Tool A')
69
+ assert.strictEqual(results[1].target.name, 'Tool B')
70
+ assert.strictEqual(totalInstalled, 0)
71
+
72
+ rmSync(dir1, { recursive: true })
73
+ rmSync(dir2, { recursive: true })
74
+ })
75
+
76
+ test('installSelected skips agents when agentsDir is null', async () => {
77
+ const skillsDir = createTempDir()
78
+
79
+ const { results } = await installSelected(
80
+ { skills: [], agents: [{ name: 'test-agent', path: 'agents/test-agent.md' }] },
81
+ [makeTarget(skillsDir, null)]
82
+ )
83
+
84
+ assert.strictEqual(results[0].target.agentsDir, null)
85
+
86
+ rmSync(skillsDir, { recursive: true })
87
+ })
88
+
89
+ test('installSelected copies skill directory to destination (happy path)', async () => {
90
+ // Create a source skill directory inside PACKAGE_ROOT (same as install.js uses)
91
+ const fixtureDir = join(PACKAGE_ROOT, '.test-fixture', 'my-test-skill')
92
+ mkdirSync(fixtureDir, { recursive: true })
93
+ writeFileSync(join(fixtureDir, 'SKILL.md'), '# Test Skill')
94
+
95
+ const destDir = createTempDir()
96
+
97
+ const { totalInstalled, results } = await installSelected(
98
+ { skills: [{ name: 'my-test-skill', path: '.test-fixture/my-test-skill' }], agents: [] },
99
+ [makeTarget(destDir, null)]
100
+ )
101
+
102
+ assert.strictEqual(totalInstalled, 1)
103
+ assert.strictEqual(results[0].installed, 1)
104
+ assert.ok(existsSync(join(destDir, 'my-test-skill', 'SKILL.md')))
105
+
106
+ rmSync(join(PACKAGE_ROOT, '.test-fixture'), { recursive: true })
107
+ rmSync(destDir, { recursive: true })
108
+ })
package/src/picker.js CHANGED
@@ -31,7 +31,7 @@ export async function pickInstallables() {
31
31
  const mode = await select({
32
32
  message: 'What to install?',
33
33
  options: [
34
- { value: 'all', label: 'Everything (24 skills + 7 agents)' },
34
+ { value: 'all', label: `Everything (${manifest.skills.length} skills + ${manifest.agents.length} agents)` },
35
35
  { value: 'skills-only', label: 'All skills only' },
36
36
  { value: 'agents-only', label: 'All agents only' },
37
37
  { value: 'pick', label: 'Let me choose...' }
@@ -42,8 +42,13 @@ export async function pickInstallables() {
42
42
  if (mode === 'skills-only') return { skills: manifest.skills, agents: [] }
43
43
  if (mode === 'agents-only') return { skills: [], agents: manifest.agents }
44
44
 
45
- // Manual pick
46
- const skillChoices = manifest.skills.map(s => ({
45
+ const sortedSkills = [...manifest.skills].sort((a, b) => {
46
+ if (a.name === 'skillkit') return -1
47
+ if (b.name === 'skillkit') return 1
48
+ return a.name.localeCompare(b.name)
49
+ })
50
+
51
+ const skillChoices = sortedSkills.map(s => ({
47
52
  value: s.name,
48
53
  label: `${getCategoryDisplay(s)} ${s.name}`,
49
54
  hint: s.description.slice(0, 60) + (s.description.length > 60 ? '…' : '')
@@ -68,7 +73,7 @@ export async function pickInstallables() {
68
73
  })
69
74
 
70
75
  return {
71
- skills: manifest.skills.filter(s => selectedSkills.includes(s.name)),
76
+ skills: sortedSkills.filter(s => selectedSkills.includes(s.name)),
72
77
  agents: manifest.agents.filter(a => selectedAgents.includes(a.name))
73
78
  }
74
79
  }
package/src/scope.js CHANGED
@@ -1,57 +1,26 @@
1
+ // installer/src/scope.js
1
2
  import { select, log } from '@clack/prompts'
2
- import { homedir } from 'os'
3
- import { join } from 'path'
4
- import { existsSync } from 'fs'
5
-
6
- export async function selectScope() {
7
- const userSkillsPath = join(homedir(), '.claude', 'skills')
8
- const projectSkillsPath = join(process.cwd(), '.claude', 'skills')
9
- const projectExists = existsSync(join(process.cwd(), '.claude', 'skills'))
10
3
 
4
+ export async function selectScope(selectedTools = []) {
11
5
  const scope = await select({
12
6
  message: 'Install to:',
13
7
  options: [
14
8
  {
15
9
  value: 'user',
16
- label: `User scope ~/.claude/skills/`,
10
+ label: 'User scope',
17
11
  hint: 'available in all projects'
18
12
  },
19
13
  {
20
14
  value: 'project',
21
- label: `Project scope ./.claude/skills/`,
22
- hint: projectExists ? 'detected .claude/skills/ here' : `will create ${projectSkillsPath}`
15
+ label: 'Project scope',
16
+ hint: 'this project only'
23
17
  }
24
18
  ]
25
19
  })
26
20
 
27
- if (scope === 'user') {
28
- return {
29
- scope: 'user',
30
- skillsDir: userSkillsPath,
31
- agentsDir: join(homedir(), '.claude', 'agents')
32
- }
33
- }
34
-
35
- return {
36
- scope: 'project',
37
- skillsDir: projectSkillsPath,
38
- agentsDir: join(process.cwd(), '.claude', 'agents')
39
- }
40
- }
41
-
42
- // Export for testing
43
- export function getUserScope() {
44
- return {
45
- scope: 'user',
46
- skillsDir: join(homedir(), '.claude', 'skills'),
47
- agentsDir: join(homedir(), '.claude', 'agents')
21
+ if (selectedTools.includes('codex') && scope === 'project') {
22
+ log.warn('Codex does not support project scope — skills will be installed to ~/.agents/skills/skillkit/')
48
23
  }
49
- }
50
24
 
51
- export function getProjectScope() {
52
- return {
53
- scope: 'project',
54
- skillsDir: join(process.cwd(), '.claude', 'skills'),
55
- agentsDir: join(process.cwd(), '.claude', 'agents')
56
- }
25
+ return scope
57
26
  }
package/src/scope.test.js CHANGED
@@ -1,19 +1,15 @@
1
+ // installer/src/scope.test.js
1
2
  import { test } from 'node:test'
2
3
  import assert from 'node:assert'
3
- import { homedir } from 'os'
4
- import { join } from 'path'
5
- import { getUserScope, getProjectScope } from './scope.js'
4
+ import { selectScope } from './scope.js'
6
5
 
7
- test('getUserScope returns correct paths', () => {
8
- const scope = getUserScope()
9
- assert.strictEqual(scope.scope, 'user')
10
- assert.strictEqual(scope.skillsDir, join(homedir(), '.claude', 'skills'))
11
- assert.strictEqual(scope.agentsDir, join(homedir(), '.claude', 'agents'))
6
+ test('selectScope is an async function', () => {
7
+ const AsyncFunction = (async () => {}).constructor
8
+ assert.ok(selectScope instanceof AsyncFunction)
12
9
  })
13
10
 
14
- test('getProjectScope returns correct paths', () => {
15
- const scope = getProjectScope()
16
- assert.strictEqual(scope.scope, 'project')
17
- assert.strictEqual(scope.skillsDir, join(process.cwd(), '.claude', 'skills'))
18
- assert.strictEqual(scope.agentsDir, join(process.cwd(), '.claude', 'agents'))
11
+ test('getUserScope and getProjectScope are no longer exported', async () => {
12
+ const mod = await import('./scope.js')
13
+ assert.strictEqual(mod.getUserScope, undefined)
14
+ assert.strictEqual(mod.getProjectScope, undefined)
19
15
  })
package/src/tools.js ADDED
@@ -0,0 +1,76 @@
1
+ // installer/src/tools.js
2
+ import { multiselect } from '@clack/prompts'
3
+ import { homedir } from 'os'
4
+ import { join } from 'path'
5
+
6
+ export async function selectTools() {
7
+ return multiselect({
8
+ message: 'Install to which tools?',
9
+ options: [
10
+ { value: 'claude-code', label: 'Claude Code', hint: '~/.claude/skills/' },
11
+ { value: 'opencode', label: 'OpenCode', hint: '~/.config/opencode/skills/' },
12
+ { value: 'codex', label: 'Codex', hint: '~/.agents/skills/' },
13
+ { value: 'copilot', label: 'GitHub Copilot', hint: '~/.copilot/skills/' }
14
+ ],
15
+ required: true
16
+ })
17
+ }
18
+
19
+ export function getToolTargets(selectedTools, scope) {
20
+ if (!selectedTools.length) return []
21
+
22
+ const home = homedir()
23
+ const cwd = process.cwd()
24
+ const isUser = scope === 'user'
25
+
26
+ const resolve = {
27
+ 'claude-code': {
28
+ name: 'Claude Code',
29
+ scope,
30
+ skillsDir: isUser ? join(home, '.claude', 'skills') : join(cwd, '.claude', 'skills'),
31
+ agentsDir: isUser ? join(home, '.claude', 'agents') : join(cwd, '.claude', 'agents')
32
+ },
33
+ 'opencode': {
34
+ name: 'OpenCode',
35
+ scope,
36
+ skillsDir: isUser
37
+ ? join(home, '.config', 'opencode', 'skills', 'skillkit')
38
+ : join(cwd, '.opencode', 'skills'),
39
+ agentsDir: isUser
40
+ ? join(home, '.config', 'opencode', 'agents')
41
+ : join(cwd, '.opencode', 'agents')
42
+ },
43
+ 'codex': {
44
+ name: 'Codex',
45
+ scope: 'user',
46
+ skillsDir: join(home, '.agents', 'skills', 'skillkit'),
47
+ agentsDir: null
48
+ },
49
+ 'copilot': {
50
+ name: 'GitHub Copilot',
51
+ scope: isUser ? 'user' : 'project',
52
+ skillsDir: join(home, '.copilot', 'skills'),
53
+ agentsDir: isUser
54
+ ? join(home, '.claude', 'agents')
55
+ : join(cwd, '.github', 'agents')
56
+ }
57
+ }
58
+
59
+ const targets = selectedTools
60
+ .filter(id => resolve[id])
61
+ .map(id => ({ ...resolve[id] }))
62
+
63
+ // Dedup: if skillsDir or agentsDir appears more than once, null it in later targets
64
+ const seenSkills = new Set()
65
+ const seenAgents = new Set()
66
+
67
+ for (const t of targets) {
68
+ if (seenSkills.has(t.skillsDir)) t.skillsDir = null
69
+ else seenSkills.add(t.skillsDir)
70
+
71
+ if (t.agentsDir && seenAgents.has(t.agentsDir)) t.agentsDir = null
72
+ else if (t.agentsDir) seenAgents.add(t.agentsDir)
73
+ }
74
+
75
+ return targets.filter(t => t.skillsDir || t.agentsDir)
76
+ }
@@ -0,0 +1,80 @@
1
+ // installer/src/tools.test.js
2
+ import { test } from 'node:test'
3
+ import assert from 'node:assert'
4
+ import { homedir } from 'os'
5
+ import { join } from 'path'
6
+ import { getToolTargets } from './tools.js'
7
+
8
+ const home = homedir()
9
+ const cwd = process.cwd()
10
+
11
+ test('getToolTargets returns empty array for empty selection', () => {
12
+ const result = getToolTargets([], 'user')
13
+ assert.deepStrictEqual(result, [])
14
+ })
15
+
16
+ test('getToolTargets claude-code user scope', () => {
17
+ const [t] = getToolTargets(['claude-code'], 'user')
18
+ assert.strictEqual(t.name, 'Claude Code')
19
+ assert.strictEqual(t.scope, 'user')
20
+ assert.strictEqual(t.skillsDir, join(home, '.claude', 'skills'))
21
+ assert.strictEqual(t.agentsDir, join(home, '.claude', 'agents'))
22
+ })
23
+
24
+ test('getToolTargets claude-code project scope', () => {
25
+ const [t] = getToolTargets(['claude-code'], 'project')
26
+ assert.strictEqual(t.scope, 'project')
27
+ assert.strictEqual(t.skillsDir, join(cwd, '.claude', 'skills'))
28
+ assert.strictEqual(t.agentsDir, join(cwd, '.claude', 'agents'))
29
+ })
30
+
31
+ test('getToolTargets opencode user scope', () => {
32
+ const [t] = getToolTargets(['opencode'], 'user')
33
+ assert.strictEqual(t.name, 'OpenCode')
34
+ assert.strictEqual(t.skillsDir, join(home, '.config', 'opencode', 'skills', 'skillkit'))
35
+ assert.strictEqual(t.agentsDir, join(home, '.config', 'opencode', 'agents'))
36
+ })
37
+
38
+ test('getToolTargets opencode project scope', () => {
39
+ const [t] = getToolTargets(['opencode'], 'project')
40
+ assert.strictEqual(t.skillsDir, join(cwd, '.opencode', 'skills'))
41
+ assert.strictEqual(t.agentsDir, join(cwd, '.opencode', 'agents'))
42
+ })
43
+
44
+ test('getToolTargets codex user scope — agentsDir is null', () => {
45
+ const [t] = getToolTargets(['codex'], 'user')
46
+ assert.strictEqual(t.name, 'Codex')
47
+ assert.strictEqual(t.skillsDir, join(home, '.agents', 'skills', 'skillkit'))
48
+ assert.strictEqual(t.agentsDir, null)
49
+ })
50
+
51
+ test('getToolTargets codex project scope — still resolves to user path', () => {
52
+ const [t] = getToolTargets(['codex'], 'project')
53
+ assert.strictEqual(t.scope, 'user')
54
+ assert.strictEqual(t.skillsDir, join(home, '.agents', 'skills', 'skillkit'))
55
+ assert.strictEqual(t.agentsDir, null)
56
+ })
57
+
58
+ test('getToolTargets copilot user scope', () => {
59
+ const [t] = getToolTargets(['copilot'], 'user')
60
+ assert.strictEqual(t.name, 'GitHub Copilot')
61
+ assert.strictEqual(t.skillsDir, join(home, '.copilot', 'skills'))
62
+ assert.strictEqual(t.agentsDir, join(home, '.claude', 'agents'))
63
+ })
64
+
65
+ test('getToolTargets copilot project scope — skills still user path, agents project path', () => {
66
+ const [t] = getToolTargets(['copilot'], 'project')
67
+ assert.strictEqual(t.skillsDir, join(home, '.copilot', 'skills'))
68
+ assert.strictEqual(t.agentsDir, join(cwd, '.github', 'agents'))
69
+ })
70
+
71
+ test('getToolTargets deduplicates agentsDir for claude-code + copilot (user)', () => {
72
+ const targets = getToolTargets(['claude-code', 'copilot'], 'user')
73
+ assert.strictEqual(targets.length, 2)
74
+ const agentsDirs = targets.map(t => t.agentsDir).filter(Boolean)
75
+ const uniqueAgentsDirs = [...new Set(agentsDirs)]
76
+ assert.strictEqual(agentsDirs.length, uniqueAgentsDirs.length, 'no duplicate agentsDirs')
77
+ // Claude Code gets agentsDir, Copilot gets null (deduped)
78
+ assert.strictEqual(targets[0].agentsDir, join(home, '.claude', 'agents'))
79
+ assert.strictEqual(targets[1].agentsDir, null)
80
+ })