@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.
- package/package.json +7 -2
- package/skills/quick-spec/tests/__pycache__/test_skill.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/skillkit/.claude/settings.local.json +7 -0
- package/skills/skillkit/scripts/__pycache__/decision_helper.cpython-314.pyc +0 -0
- package/skills/skillkit/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
- package/skills/skillkit/scripts/__pycache__/quick_validate.cpython-314.pyc +0 -0
- package/skills/skillkit/scripts/__pycache__/test_generator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/budget_tracker.cpython-312.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/budget_tracker.cpython-314.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/output_formatter.cpython-312.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/output_formatter.cpython-314.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/reference_validator.cpython-312.pyc +0 -0
- package/skills/skillkit/scripts/utils/__pycache__/reference_validator.cpython-314.pyc +0 -0
- package/skills-manifest.json +1 -1
- package/src/banner.js +1 -1
- package/src/cli.js +15 -4
- package/src/install.js +45 -29
- package/src/install.test.js +75 -7
- package/src/picker.js +9 -4
- package/src/scope.js +8 -39
- package/src/scope.test.js +9 -13
- package/src/tools.js +76 -0
- 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.
|
|
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": [
|
|
34
|
+
"keywords": [
|
|
35
|
+
"claude-code",
|
|
36
|
+
"skills",
|
|
37
|
+
"ai-agent",
|
|
38
|
+
"skillkit"
|
|
39
|
+
]
|
|
35
40
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/skills-manifest.json
CHANGED
package/src/banner.js
CHANGED
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
|
|
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
|
|
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! ${
|
|
29
|
-
log.info(
|
|
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 },
|
|
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
|
-
|
|
18
|
-
|
|
18
|
+
const results = []
|
|
19
|
+
let totalInstalled = 0
|
|
19
20
|
|
|
20
|
-
for (const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 ${
|
|
61
|
+
s.stop(`Installed ${totalInstalled} item(s)`)
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
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 {
|
|
68
|
+
return { results, totalInstalled }
|
|
53
69
|
}
|
package/src/install.test.js
CHANGED
|
@@ -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: [] },
|
|
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' },
|
|
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
|
|
46
|
+
const { totalInstalled, results } = await installSelected(
|
|
31
47
|
{ skills: [{ name: null }, { name: 'valid', path: 'skills/valid' }], agents: [] },
|
|
32
|
-
|
|
48
|
+
[makeTarget(skillsDir, agentsDir)]
|
|
33
49
|
)
|
|
34
50
|
|
|
35
|
-
assert.strictEqual(
|
|
36
|
-
assert.strictEqual(
|
|
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:
|
|
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
|
-
|
|
46
|
-
|
|
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:
|
|
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:
|
|
10
|
+
label: 'User scope',
|
|
17
11
|
hint: 'available in all projects'
|
|
18
12
|
},
|
|
19
13
|
{
|
|
20
14
|
value: 'project',
|
|
21
|
-
label:
|
|
22
|
-
hint:
|
|
15
|
+
label: 'Project scope',
|
|
16
|
+
hint: 'this project only'
|
|
23
17
|
}
|
|
24
18
|
]
|
|
25
19
|
})
|
|
26
20
|
|
|
27
|
-
if (scope === '
|
|
28
|
-
|
|
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
|
-
|
|
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 {
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { getUserScope, getProjectScope } from './scope.js'
|
|
4
|
+
import { selectScope } from './scope.js'
|
|
6
5
|
|
|
7
|
-
test('
|
|
8
|
-
const
|
|
9
|
-
assert.
|
|
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
|
|
15
|
-
const
|
|
16
|
-
assert.strictEqual(
|
|
17
|
-
assert.strictEqual(
|
|
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
|
+
})
|