happyskills 0.13.0 → 0.14.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/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/agents/detector.js +99 -0
- package/src/agents/index.js +9 -0
- package/src/agents/linker.js +137 -0
- package/src/agents/registry.js +81 -0
- package/src/api/repos.js +1 -0
- package/src/commands/install.js +9 -6
- package/src/commands/refresh.js +1 -1
- package/src/commands/setup.js +1 -0
- package/src/commands/uninstall.js +6 -3
- package/src/commands/update.js +1 -0
- package/src/commands/whoami.js +3 -1
- package/src/config/paths.js +11 -1
- package/src/engine/installer.js +27 -3
- package/src/engine/uninstaller.js +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.0] - 2026-03-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add multi-agent support — auto-detect and symlink skills to Cursor, Windsurf, Codex, GitHub Copilot, Aider, Cline, and Roo Code alongside Claude Code
|
|
14
|
+
- Add `--agents` flag to `install`, `uninstall`, `update`, `refresh`, and `setup` commands for explicit agent targeting (e.g., `--agents claude,cursor`)
|
|
15
|
+
- Add `HAPPYSKILLS_AGENTS` environment variable for persistent agent preference
|
|
16
|
+
- Add `linked_agents` field to install JSON response showing which secondary agents received symlinks
|
|
17
|
+
- Add `cli/src/agents/` module with agent registry (8 agents), auto-detection via home directory scan, and symlink/copy linker with Windows fallback
|
|
18
|
+
|
|
19
|
+
## [0.13.1] - 2026-03-24
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Fix `search --type` filter being silently ignored — the `type` parameter was never forwarded to the API, so `--type kit` and `--type skill` had no effect
|
|
23
|
+
- Fix `whoami` incorrectly labeling all personal-type workspaces as "(personal)" even when the user is only a collaborator — now uses ownership from the API so only workspaces the user owns show "(personal)" or "(organization)"
|
|
24
|
+
|
|
10
25
|
## [0.13.0] - 2026-03-12
|
|
11
26
|
|
|
12
27
|
### Added
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
5
|
+
const { AGENTS, PRIMARY_AGENT, get_agent } = require('./registry')
|
|
6
|
+
|
|
7
|
+
const home_dir = os.homedir()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect which agents are installed by checking for home-level config folders.
|
|
11
|
+
* All checks run in parallel via Promise.all.
|
|
12
|
+
*
|
|
13
|
+
* @returns {Promise} [errors, Agent[]] — array of detected agent objects
|
|
14
|
+
*/
|
|
15
|
+
const detect_agents = () => catch_errors('Agent detection failed', async () => {
|
|
16
|
+
const checks = AGENTS.map(async (agent) => {
|
|
17
|
+
for (const rel_path of agent.detect_paths) {
|
|
18
|
+
const full_path = path.join(home_dir, rel_path)
|
|
19
|
+
try {
|
|
20
|
+
await fs.promises.access(full_path)
|
|
21
|
+
return agent
|
|
22
|
+
} catch {
|
|
23
|
+
// path not found — agent not detected
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const results = await Promise.all(checks)
|
|
30
|
+
return results.filter(Boolean)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a comma-separated agent list and validate each id against the registry.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} agents_str — e.g. "claude,cursor,windsurf"
|
|
37
|
+
* @returns {{ primary: Agent, secondaries: Agent[] }}
|
|
38
|
+
*/
|
|
39
|
+
const _parse_agents_flag = (agents_str) => {
|
|
40
|
+
const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
|
|
41
|
+
const agents = []
|
|
42
|
+
const unknown = []
|
|
43
|
+
|
|
44
|
+
for (const id of ids) {
|
|
45
|
+
const agent = get_agent(id)
|
|
46
|
+
if (agent) agents.push(agent)
|
|
47
|
+
else unknown.push(id)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (unknown.length > 0) {
|
|
51
|
+
throw new Error(`Unknown agent(s): ${unknown.join(', ')}. Available: ${AGENTS.map(a => a.id).join(', ')}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ensure primary is always included
|
|
55
|
+
if (!agents.find(a => a.primary)) {
|
|
56
|
+
agents.unshift(PRIMARY_AGENT)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const primary = agents.find(a => a.primary)
|
|
60
|
+
const secondaries = agents.filter(a => !a.primary)
|
|
61
|
+
return { primary, secondaries }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve which agents to target. Priority:
|
|
66
|
+
* 1. --agents flag (explicit per-command override)
|
|
67
|
+
* 2. HAPPYSKILLS_AGENTS env var (persistent user preference)
|
|
68
|
+
* 3. Auto-detection (scan home dir for agent config folders)
|
|
69
|
+
*
|
|
70
|
+
* Always includes the primary agent (Claude).
|
|
71
|
+
*
|
|
72
|
+
* @param {string|undefined} agents_flag — from --agents CLI flag
|
|
73
|
+
* @returns {Promise} [errors, { primary: Agent, secondaries: Agent[] }]
|
|
74
|
+
*/
|
|
75
|
+
const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed', async () => {
|
|
76
|
+
// 1. Explicit --agents flag takes highest priority
|
|
77
|
+
if (agents_flag) {
|
|
78
|
+
return _parse_agents_flag(agents_flag)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. HAPPYSKILLS_AGENTS env var
|
|
82
|
+
const env_agents = process.env.HAPPYSKILLS_AGENTS
|
|
83
|
+
if (env_agents) {
|
|
84
|
+
return _parse_agents_flag(env_agents)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Auto-detect
|
|
88
|
+
const [errors, detected] = await detect_agents()
|
|
89
|
+
if (errors) throw errors[0]
|
|
90
|
+
|
|
91
|
+
// Ensure primary is always included
|
|
92
|
+
const all = detected.find(a => a.primary) ? detected : [PRIMARY_AGENT, ...detected]
|
|
93
|
+
|
|
94
|
+
const primary = all.find(a => a.primary)
|
|
95
|
+
const secondaries = all.filter(a => !a.primary)
|
|
96
|
+
return { primary, secondaries }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
module.exports = { detect_agents, resolve_agents }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids } = require('./registry')
|
|
2
|
+
const { detect_agents, resolve_agents } = require('./detector')
|
|
3
|
+
const { link_to_agents, unlink_from_agents, is_symlink } = require('./linker')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids,
|
|
7
|
+
detect_agents, resolve_agents,
|
|
8
|
+
link_to_agents, unlink_from_agents, is_symlink
|
|
9
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
4
|
+
const { agent_skill_install_dir } = require('../config/paths')
|
|
5
|
+
const { ensure_dir, remove_dir } = require('../utils/fs')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a path is a symlink.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} target_path
|
|
11
|
+
* @returns {Promise} [errors, boolean]
|
|
12
|
+
*/
|
|
13
|
+
const is_symlink = (target_path) => catch_errors('Symlink check failed', async () => {
|
|
14
|
+
try {
|
|
15
|
+
const stats = await fs.promises.lstat(target_path)
|
|
16
|
+
return stats.isSymbolicLink()
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a path exists (file, dir, or symlink).
|
|
24
|
+
*
|
|
25
|
+
* @param {string} target_path
|
|
26
|
+
* @returns {Promise<boolean>}
|
|
27
|
+
*/
|
|
28
|
+
const _exists = async (target_path) => {
|
|
29
|
+
try {
|
|
30
|
+
await fs.promises.lstat(target_path)
|
|
31
|
+
return true
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a symlink, falling back to a physical copy on failure.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} source — absolute path to the primary skill dir
|
|
41
|
+
* @param {string} target — absolute path to the secondary agent's skill dir
|
|
42
|
+
* @returns {{ method: string, error?: string }}
|
|
43
|
+
*/
|
|
44
|
+
const _link_or_copy = async (source, target) => {
|
|
45
|
+
try {
|
|
46
|
+
// 'junction' works on Windows without admin; ignored on macOS/Linux
|
|
47
|
+
await fs.promises.symlink(source, target, 'junction')
|
|
48
|
+
return { method: 'symlink' }
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.code === 'EPERM' || err.code === 'ENOTSUP') {
|
|
51
|
+
// Symlinks not supported — fall back to physical copy
|
|
52
|
+
await fs.promises.cp(source, target, { recursive: true })
|
|
53
|
+
return { method: 'copy' }
|
|
54
|
+
}
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Link a skill to secondary agents. Creates symlinks (or copies as fallback)
|
|
61
|
+
* from the primary install location to each secondary agent's skills directory.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} source_dir — absolute path to the primary skill dir (e.g. .claude/skills/deploy-aws)
|
|
64
|
+
* @param {Agent[]} secondaries — array of secondary agent objects
|
|
65
|
+
* @param {object} options — { global, project_root, skill_name }
|
|
66
|
+
* @returns {Promise} [errors, { agent_id, path, method }[]]
|
|
67
|
+
*/
|
|
68
|
+
const link_to_agents = (source_dir, secondaries, options = {}) => catch_errors('Agent linking failed', async () => {
|
|
69
|
+
const { global: is_global = false, project_root, skill_name } = options
|
|
70
|
+
const results = []
|
|
71
|
+
|
|
72
|
+
for (const agent of secondaries) {
|
|
73
|
+
const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
|
|
74
|
+
|
|
75
|
+
// If target already exists and is a symlink pointing to source, skip
|
|
76
|
+
const [, is_link] = await is_symlink(target)
|
|
77
|
+
if (is_link) {
|
|
78
|
+
try {
|
|
79
|
+
const link_target = await fs.promises.readlink(target)
|
|
80
|
+
if (link_target === source_dir) {
|
|
81
|
+
results.push({ agent_id: agent.id, path: target, method: 'symlink', skipped: true })
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// readlink failed — remove and recreate
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Remove stale target (dir or broken symlink)
|
|
90
|
+
if (await _exists(target)) {
|
|
91
|
+
await remove_dir(target)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ensure parent dir exists
|
|
95
|
+
await ensure_dir(path.dirname(target))
|
|
96
|
+
|
|
97
|
+
const { method } = await _link_or_copy(source_dir, target)
|
|
98
|
+
results.push({ agent_id: agent.id, path: target, method })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Unlink a skill from secondary agents. Removes symlinks or physical copies.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} skill_name — skill directory name (e.g. "deploy-aws")
|
|
108
|
+
* @param {Agent[]} secondaries — array of secondary agent objects
|
|
109
|
+
* @param {object} options — { global, project_root }
|
|
110
|
+
* @returns {Promise} [errors, { agent_id, removed: boolean }[]]
|
|
111
|
+
*/
|
|
112
|
+
const unlink_from_agents = (skill_name, secondaries, options = {}) => catch_errors('Agent unlinking failed', async () => {
|
|
113
|
+
const { global: is_global = false, project_root } = options
|
|
114
|
+
const results = []
|
|
115
|
+
|
|
116
|
+
for (const agent of secondaries) {
|
|
117
|
+
const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
|
|
118
|
+
|
|
119
|
+
if (!(await _exists(target))) {
|
|
120
|
+
results.push({ agent_id: agent.id, removed: false })
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const [, is_link] = await is_symlink(target)
|
|
125
|
+
if (is_link) {
|
|
126
|
+
await fs.promises.unlink(target)
|
|
127
|
+
} else {
|
|
128
|
+
await remove_dir(target)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
results.push({ agent_id: agent.id, removed: true })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
module.exports = { link_to_agents, unlink_from_agents, is_symlink }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent registry — pure-data definitions for supported AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Each entry describes where an agent stores skills and how to detect it.
|
|
5
|
+
* Adding a new agent = adding one object to the AGENTS array.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const AGENTS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'claude',
|
|
11
|
+
display_name: 'Claude Code',
|
|
12
|
+
skills_dir: '.claude/skills',
|
|
13
|
+
global_skills_dir: '.claude/skills',
|
|
14
|
+
detect_paths: ['.claude'],
|
|
15
|
+
primary: true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'cursor',
|
|
19
|
+
display_name: 'Cursor',
|
|
20
|
+
skills_dir: '.cursor/skills',
|
|
21
|
+
global_skills_dir: '.cursor/skills',
|
|
22
|
+
detect_paths: ['.cursor'],
|
|
23
|
+
primary: false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'windsurf',
|
|
27
|
+
display_name: 'Windsurf',
|
|
28
|
+
skills_dir: '.windsurf/skills',
|
|
29
|
+
global_skills_dir: '.windsurf/skills',
|
|
30
|
+
detect_paths: ['.windsurf'],
|
|
31
|
+
primary: false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'codex',
|
|
35
|
+
display_name: 'Codex',
|
|
36
|
+
skills_dir: '.codex/skills',
|
|
37
|
+
global_skills_dir: '.codex/skills',
|
|
38
|
+
detect_paths: ['.codex'],
|
|
39
|
+
primary: false
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'copilot',
|
|
43
|
+
display_name: 'GitHub Copilot',
|
|
44
|
+
skills_dir: '.github/skills',
|
|
45
|
+
global_skills_dir: '.github/skills',
|
|
46
|
+
detect_paths: ['.github/copilot'],
|
|
47
|
+
primary: false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'aider',
|
|
51
|
+
display_name: 'Aider',
|
|
52
|
+
skills_dir: '.aider/skills',
|
|
53
|
+
global_skills_dir: '.aider/skills',
|
|
54
|
+
detect_paths: ['.aider'],
|
|
55
|
+
primary: false
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'cline',
|
|
59
|
+
display_name: 'Cline',
|
|
60
|
+
skills_dir: '.cline/skills',
|
|
61
|
+
global_skills_dir: '.cline/skills',
|
|
62
|
+
detect_paths: ['.cline'],
|
|
63
|
+
primary: false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'roo',
|
|
67
|
+
display_name: 'Roo Code',
|
|
68
|
+
skills_dir: '.roo/skills',
|
|
69
|
+
global_skills_dir: '.roo/skills',
|
|
70
|
+
detect_paths: ['.roo'],
|
|
71
|
+
primary: false
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const PRIMARY_AGENT = AGENTS.find(a => a.primary)
|
|
76
|
+
|
|
77
|
+
const get_agent = (id) => AGENTS.find(a => a.id === id) || null
|
|
78
|
+
|
|
79
|
+
const get_all_agent_ids = () => AGENTS.map(a => a.id)
|
|
80
|
+
|
|
81
|
+
module.exports = { AGENTS, PRIMARY_AGENT, get_agent, get_all_agent_ids }
|
package/src/api/repos.js
CHANGED
|
@@ -9,6 +9,7 @@ const search = (query, options = {}) => catch_errors('Search failed', async () =
|
|
|
9
9
|
if (options.scope) params.set('scope', options.scope)
|
|
10
10
|
if (options.workspace) params.set('workspace', options.workspace)
|
|
11
11
|
if (options.tags) params.set('tags', options.tags)
|
|
12
|
+
if (options.type) params.set('type', options.type)
|
|
12
13
|
const needs_auth = (options.scope && options.scope !== 'public') || options.workspace
|
|
13
14
|
const [errors, data] = await client.get(`/repos/search?${params}`, { auth: needs_auth || false })
|
|
14
15
|
if (errors) throw errors[errors.length - 1]
|
package/src/commands/install.js
CHANGED
|
@@ -18,12 +18,14 @@ Arguments:
|
|
|
18
18
|
owner/skill@latest Force install of the latest version (bypasses lock skip check)
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
|
-
-g, --global
|
|
22
|
-
--version <ver>
|
|
23
|
-
--force
|
|
24
|
-
--fresh
|
|
25
|
-
-
|
|
26
|
-
|
|
21
|
+
-g, --global Install globally (~/.claude/skills/)
|
|
22
|
+
--version <ver> Pin to specific version (overrides inline @version)
|
|
23
|
+
--force Force install on dependency conflicts
|
|
24
|
+
--fresh Ignore lock file, re-resolve from scratch
|
|
25
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
26
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
27
|
+
-y, --yes Skip confirmation prompts
|
|
28
|
+
--json Output as JSON
|
|
27
29
|
|
|
28
30
|
Aliases: i, add
|
|
29
31
|
|
|
@@ -58,6 +60,7 @@ const run = (args) => catch_errors('Install failed', async () => {
|
|
|
58
60
|
force: args.flags.force || false,
|
|
59
61
|
fresh: args.flags.fresh || false,
|
|
60
62
|
yes: args.flags.yes || false,
|
|
63
|
+
agents: args.flags.agents || undefined,
|
|
61
64
|
project_root: find_project_root()
|
|
62
65
|
}
|
|
63
66
|
|
package/src/commands/refresh.js
CHANGED
|
@@ -126,7 +126,7 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// 5. Update outdated skills
|
|
129
|
-
const options = { global: is_global, fresh: true, project_root }
|
|
129
|
+
const options = { global: is_global, fresh: true, agents: args.flags.agents || undefined, project_root }
|
|
130
130
|
const updated = []
|
|
131
131
|
const update_errors = []
|
|
132
132
|
|
package/src/commands/setup.js
CHANGED
|
@@ -13,9 +13,11 @@ Arguments:
|
|
|
13
13
|
owner/skill Skill to remove (e.g., acme/deploy-aws)
|
|
14
14
|
|
|
15
15
|
Options:
|
|
16
|
-
-g, --global
|
|
17
|
-
-
|
|
18
|
-
|
|
16
|
+
-g, --global Remove from global scope
|
|
17
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
18
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
19
|
+
-y, --yes Skip confirmation prompts
|
|
20
|
+
--json Output as JSON
|
|
19
21
|
|
|
20
22
|
Aliases: rm, remove
|
|
21
23
|
|
|
@@ -40,6 +42,7 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
|
|
|
40
42
|
|
|
41
43
|
const options = {
|
|
42
44
|
global: args.flags.global || false,
|
|
45
|
+
agents: args.flags.agents || undefined,
|
|
43
46
|
project_root: find_project_root()
|
|
44
47
|
}
|
|
45
48
|
|
package/src/commands/update.js
CHANGED
package/src/commands/whoami.js
CHANGED
|
@@ -59,7 +59,9 @@ const run = (args) => catch_errors('Whoami failed', async () => {
|
|
|
59
59
|
console.log()
|
|
60
60
|
print_label('Workspaces', '')
|
|
61
61
|
for (const ws of workspaces) {
|
|
62
|
-
const type_label = ws.
|
|
62
|
+
const type_label = ws.is_owner
|
|
63
|
+
? ws.type === 'personal' ? ' (personal)' : ' (organization)'
|
|
64
|
+
: ''
|
|
63
65
|
console.log(` ${ws.slug}${type_label}`)
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/config/paths.js
CHANGED
|
@@ -36,6 +36,14 @@ const skill_install_dir = (base_skills_dir, name) => path.join(base_skills_dir,
|
|
|
36
36
|
|
|
37
37
|
const find_project_root = (start_dir = process.cwd()) => path.resolve(start_dir)
|
|
38
38
|
|
|
39
|
+
const agent_skills_dir = (agent, global = false, project_root) => {
|
|
40
|
+
if (global) return path.join(home_dir, agent.global_skills_dir)
|
|
41
|
+
return path.join(project_root || process.cwd(), agent.skills_dir)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agent_skill_install_dir = (agent, global, project_root, skill_name) =>
|
|
45
|
+
path.join(agent_skills_dir(agent, global, project_root), skill_name)
|
|
46
|
+
|
|
39
47
|
module.exports = {
|
|
40
48
|
home_dir,
|
|
41
49
|
config_dir,
|
|
@@ -49,5 +57,7 @@ module.exports = {
|
|
|
49
57
|
lock_root,
|
|
50
58
|
lock_file_path,
|
|
51
59
|
skill_install_dir,
|
|
52
|
-
find_project_root
|
|
60
|
+
find_project_root,
|
|
61
|
+
agent_skills_dir,
|
|
62
|
+
agent_skill_install_dir
|
|
53
63
|
}
|
package/src/engine/installer.js
CHANGED
|
@@ -11,8 +11,9 @@ const { write_lock, update_lock_skills } = require('../lock/writer')
|
|
|
11
11
|
const { skills_dir, tmp_dir, skill_install_dir, lock_root } = require('../config/paths')
|
|
12
12
|
const { ensure_dir, remove_dir, file_exists, read_json } = require('../utils/fs')
|
|
13
13
|
const { SKILL_JSON } = require('../constants')
|
|
14
|
+
const { resolve_agents, link_to_agents } = require('../agents')
|
|
14
15
|
const { create_spinner } = require('../ui/spinner')
|
|
15
|
-
const { print_success, print_warn } = require('../ui/output')
|
|
16
|
+
const { print_success, print_warn, print_info } = require('../ui/output')
|
|
16
17
|
|
|
17
18
|
const _format_warnings = (missing_deps) => (missing_deps || []).map(dep => {
|
|
18
19
|
const hint = dep.install_hint ? ` (install: ${dep.install_hint})` : ''
|
|
@@ -20,8 +21,13 @@ const _format_warnings = (missing_deps) => (missing_deps || []).map(dep => {
|
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const install = (skill, options = {}) => catch_errors('Install failed', async () => {
|
|
23
|
-
const { version, global: is_global = false, force = false, fresh = false, project_root } = options
|
|
24
|
+
const { version, global: is_global = false, force = false, fresh = false, project_root, agents: agents_flag } = options
|
|
24
25
|
const base_dir = skills_dir(is_global, project_root)
|
|
26
|
+
|
|
27
|
+
// Resolve target agents (primary + secondaries)
|
|
28
|
+
const [agents_err, agents_result] = await resolve_agents(agents_flag)
|
|
29
|
+
if (agents_err) throw e('Agent resolution failed', agents_err)
|
|
30
|
+
const { secondaries } = agents_result
|
|
25
31
|
const temp_dir = tmp_dir(base_dir)
|
|
26
32
|
const lock_dir = lock_root(is_global, project_root)
|
|
27
33
|
|
|
@@ -130,6 +136,19 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
130
136
|
await fs.promises.rename(tmp_path, final_dir)
|
|
131
137
|
}
|
|
132
138
|
|
|
139
|
+
// Link to secondary agents (non-fatal — warnings only)
|
|
140
|
+
if (secondaries.length > 0) {
|
|
141
|
+
spinner.update(`Linking to ${secondaries.length} agent(s)...`)
|
|
142
|
+
for (const { pkg } of downloaded) {
|
|
143
|
+
const name = pkg.skill.split('/')[1]
|
|
144
|
+
const source = skill_install_dir(base_dir, name)
|
|
145
|
+
const [link_errs] = await link_to_agents(source, secondaries, { global: is_global, project_root, skill_name: name })
|
|
146
|
+
if (link_errs) {
|
|
147
|
+
print_warn(`Warning: failed to link ${pkg.skill} to some agents`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
spinner.update('Writing lock file...')
|
|
134
153
|
|
|
135
154
|
const updates = {}
|
|
@@ -162,6 +181,10 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
162
181
|
|
|
163
182
|
spinner.succeed(`Installed ${packages_to_install.length} package(s)`)
|
|
164
183
|
|
|
184
|
+
if (secondaries.length > 0) {
|
|
185
|
+
print_info(`Linked to: ${secondaries.map(a => a.display_name).join(', ')}`)
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
const [, missing_deps] = await check_system_dependencies(packages)
|
|
166
189
|
|
|
167
190
|
const forced_pkgs = packages_to_install.filter(p => p.forced)
|
|
@@ -178,7 +201,8 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
178
201
|
const warnings = _format_warnings(missing_deps)
|
|
179
202
|
const forced = forced_pkgs.map(p => ({ skill: p.skill, version: p.version }))
|
|
180
203
|
|
|
181
|
-
|
|
204
|
+
const linked_agents = secondaries.map(a => a.id)
|
|
205
|
+
return { skill, version: packages[0]?.version, no_op: false, installed, skipped, warnings, forced, packages: packages_to_install.length, linked_agents }
|
|
182
206
|
} catch (err) {
|
|
183
207
|
await remove_dir(temp_dir)
|
|
184
208
|
throw err
|
|
@@ -3,6 +3,7 @@ const { read_lock, get_locked_skill, get_all_locked_skills } = require('../lock/
|
|
|
3
3
|
const { write_lock, update_lock_skills } = require('../lock/writer')
|
|
4
4
|
const { skills_dir, skill_install_dir, lock_root } = require('../config/paths')
|
|
5
5
|
const { remove_dir } = require('../utils/fs')
|
|
6
|
+
const { resolve_agents, unlink_from_agents } = require('../agents')
|
|
6
7
|
const { print_success, print_info } = require('../ui/output')
|
|
7
8
|
|
|
8
9
|
const find_orphans = (skills, removed_skill) => {
|
|
@@ -19,7 +20,12 @@ const find_orphans = (skills, removed_skill) => {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', async () => {
|
|
22
|
-
const { global: is_global = false, project_root } = options
|
|
23
|
+
const { global: is_global = false, project_root, agents: agents_flag } = options
|
|
24
|
+
|
|
25
|
+
// Resolve target agents
|
|
26
|
+
const [agents_err, agents_result] = await resolve_agents(agents_flag)
|
|
27
|
+
if (agents_err) throw e('Agent resolution failed', agents_err)
|
|
28
|
+
const { secondaries } = agents_result
|
|
23
29
|
const base_dir = skills_dir(is_global, project_root)
|
|
24
30
|
const lock_dir = lock_root(is_global, project_root)
|
|
25
31
|
|
|
@@ -50,6 +56,13 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
|
|
|
50
56
|
if (rm_errors) throw e(`Failed to remove ${name}`, rm_errors)
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
// Unlink from secondary agents
|
|
60
|
+
if (secondaries.length > 0) {
|
|
61
|
+
for (const name of to_remove) {
|
|
62
|
+
await unlink_from_agents(name.split('/')[1], secondaries, { global: is_global, project_root })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
const updates = {}
|
|
54
67
|
for (const name of to_remove) {
|
|
55
68
|
updates[name] = null
|