happyskills 0.14.0 → 0.16.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 +17 -0
- package/package.json +1 -1
- package/src/agents/detector.js +17 -20
- package/src/agents/index.js +2 -2
- package/src/agents/linker.js +10 -10
- package/src/agents/registry.js +11 -20
- package/src/commands/config.js +212 -0
- package/src/config/paths.js +3 -3
- package/src/config/paths.test.js +5 -5
- package/src/config/store.js +76 -0
- package/src/constants.js +2 -1
- package/src/engine/installer.js +9 -9
- package/src/engine/uninstaller.js +4 -4
- package/src/integration/cli.test.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-03-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `config` command for viewing and modifying global CLI configuration (`~/.config/happyskills/config.json`)
|
|
14
|
+
- Add `config agents <list>` to set default agent targets persistently (e.g., `happyskills config agents claude,cursor`)
|
|
15
|
+
- Add `config agents --list` to show all 8 supported agents with detection and default status
|
|
16
|
+
- Add `config agents --reset` to clear agent preference and return to auto-detection
|
|
17
|
+
- Add `cli/src/config/store.js` module for reading and writing global config
|
|
18
|
+
- Integrate config file into agent resolution as priority 3 (`--agents` flag > env var > config file > auto-detect)
|
|
19
|
+
|
|
20
|
+
## [0.15.0] - 2026-03-27
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Move canonical skill install directory from `.claude/skills/` to `.agents/skills/` — physical files now live in an agent-neutral location, and ALL agents (including Claude Code) receive symlinks
|
|
24
|
+
- Move global lock root from `~/.claude/` to `~/.agents/`
|
|
25
|
+
- Remove primary/secondary agent distinction — all agents are equal, each gets a symlink from `.agents/skills/`
|
|
26
|
+
|
|
10
27
|
## [0.14.0] - 2026-03-27
|
|
11
28
|
|
|
12
29
|
### Added
|
package/package.json
CHANGED
package/src/agents/detector.js
CHANGED
|
@@ -2,7 +2,8 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const os = require('os')
|
|
4
4
|
const { error: { catch_errors } } = require('puffy-core')
|
|
5
|
-
const { AGENTS,
|
|
5
|
+
const { AGENTS, get_agent } = require('./registry')
|
|
6
|
+
const { get_config_value } = require('../config/store')
|
|
6
7
|
|
|
7
8
|
const home_dir = os.homedir()
|
|
8
9
|
|
|
@@ -34,7 +35,7 @@ const detect_agents = () => catch_errors('Agent detection failed', async () => {
|
|
|
34
35
|
* Parse a comma-separated agent list and validate each id against the registry.
|
|
35
36
|
*
|
|
36
37
|
* @param {string} agents_str — e.g. "claude,cursor,windsurf"
|
|
37
|
-
* @returns {{
|
|
38
|
+
* @returns {{ agents: Agent[] }}
|
|
38
39
|
*/
|
|
39
40
|
const _parse_agents_flag = (agents_str) => {
|
|
40
41
|
const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
|
|
@@ -51,26 +52,21 @@ const _parse_agents_flag = (agents_str) => {
|
|
|
51
52
|
throw new Error(`Unknown agent(s): ${unknown.join(', ')}. Available: ${AGENTS.map(a => a.id).join(', ')}`)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
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 }
|
|
55
|
+
return { agents }
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
/**
|
|
65
59
|
* Resolve which agents to target. Priority:
|
|
66
60
|
* 1. --agents flag (explicit per-command override)
|
|
67
61
|
* 2. HAPPYSKILLS_AGENTS env var (persistent user preference)
|
|
68
|
-
* 3.
|
|
62
|
+
* 3. Config file agents value (~/.config/happyskills/config.json)
|
|
63
|
+
* 4. Auto-detection (scan home dir for agent config folders)
|
|
69
64
|
*
|
|
70
|
-
*
|
|
65
|
+
* Physical files always go to the canonical `.agents/skills/` directory.
|
|
66
|
+
* Each resolved agent gets a symlink from the canonical location.
|
|
71
67
|
*
|
|
72
68
|
* @param {string|undefined} agents_flag — from --agents CLI flag
|
|
73
|
-
* @returns {Promise} [errors, {
|
|
69
|
+
* @returns {Promise} [errors, { agents: Agent[] }]
|
|
74
70
|
*/
|
|
75
71
|
const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed', async () => {
|
|
76
72
|
// 1. Explicit --agents flag takes highest priority
|
|
@@ -84,16 +80,17 @@ const resolve_agents = (agents_flag) => catch_errors('Agent resolution failed',
|
|
|
84
80
|
return _parse_agents_flag(env_agents)
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
// 3.
|
|
83
|
+
// 3. Config file
|
|
84
|
+
const [, config_agents] = await get_config_value('agents')
|
|
85
|
+
if (config_agents) {
|
|
86
|
+
return _parse_agents_flag(config_agents)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 4. Auto-detect
|
|
88
90
|
const [errors, detected] = await detect_agents()
|
|
89
91
|
if (errors) throw errors[0]
|
|
90
92
|
|
|
91
|
-
|
|
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 }
|
|
93
|
+
return { agents: detected }
|
|
97
94
|
})
|
|
98
95
|
|
|
99
96
|
module.exports = { detect_agents, resolve_agents }
|
package/src/agents/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const { AGENTS,
|
|
1
|
+
const { AGENTS, get_agent, get_all_agent_ids } = require('./registry')
|
|
2
2
|
const { detect_agents, resolve_agents } = require('./detector')
|
|
3
3
|
const { link_to_agents, unlink_from_agents, is_symlink } = require('./linker')
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
AGENTS,
|
|
6
|
+
AGENTS, get_agent, get_all_agent_ids,
|
|
7
7
|
detect_agents, resolve_agents,
|
|
8
8
|
link_to_agents, unlink_from_agents, is_symlink
|
|
9
9
|
}
|
package/src/agents/linker.js
CHANGED
|
@@ -57,19 +57,19 @@ const _link_or_copy = async (source, target) => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Link a skill to
|
|
61
|
-
* from the
|
|
60
|
+
* Link a skill to detected agents. Creates symlinks (or copies as fallback)
|
|
61
|
+
* from the canonical `.agents/skills/` location to each agent's skills directory.
|
|
62
62
|
*
|
|
63
|
-
* @param {string} source_dir — absolute path to the
|
|
64
|
-
* @param {Agent[]}
|
|
63
|
+
* @param {string} source_dir — absolute path to the canonical skill dir (e.g. .agents/skills/deploy-aws)
|
|
64
|
+
* @param {Agent[]} agents — array of agent objects to link
|
|
65
65
|
* @param {object} options — { global, project_root, skill_name }
|
|
66
66
|
* @returns {Promise} [errors, { agent_id, path, method }[]]
|
|
67
67
|
*/
|
|
68
|
-
const link_to_agents = (source_dir,
|
|
68
|
+
const link_to_agents = (source_dir, agents, options = {}) => catch_errors('Agent linking failed', async () => {
|
|
69
69
|
const { global: is_global = false, project_root, skill_name } = options
|
|
70
70
|
const results = []
|
|
71
71
|
|
|
72
|
-
for (const agent of
|
|
72
|
+
for (const agent of agents) {
|
|
73
73
|
const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
|
|
74
74
|
|
|
75
75
|
// If target already exists and is a symlink pointing to source, skip
|
|
@@ -102,18 +102,18 @@ const link_to_agents = (source_dir, secondaries, options = {}) => catch_errors('
|
|
|
102
102
|
})
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Unlink a skill from
|
|
105
|
+
* Unlink a skill from agents. Removes symlinks or physical copies.
|
|
106
106
|
*
|
|
107
107
|
* @param {string} skill_name — skill directory name (e.g. "deploy-aws")
|
|
108
|
-
* @param {Agent[]}
|
|
108
|
+
* @param {Agent[]} agents — array of agent objects to unlink
|
|
109
109
|
* @param {object} options — { global, project_root }
|
|
110
110
|
* @returns {Promise} [errors, { agent_id, removed: boolean }[]]
|
|
111
111
|
*/
|
|
112
|
-
const unlink_from_agents = (skill_name,
|
|
112
|
+
const unlink_from_agents = (skill_name, agents, options = {}) => catch_errors('Agent unlinking failed', async () => {
|
|
113
113
|
const { global: is_global = false, project_root } = options
|
|
114
114
|
const results = []
|
|
115
115
|
|
|
116
|
-
for (const agent of
|
|
116
|
+
for (const agent of agents) {
|
|
117
117
|
const target = agent_skill_install_dir(agent, is_global, project_root, skill_name)
|
|
118
118
|
|
|
119
119
|
if (!(await _exists(target))) {
|
package/src/agents/registry.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent registry — pure-data definitions for supported AI agents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Physical skill files live in the canonical `.agents/skills/` directory.
|
|
5
|
+
* Each agent listed here gets a symlink from `.agents/skills/` to its own skills directory.
|
|
5
6
|
* Adding a new agent = adding one object to the AGENTS array.
|
|
6
7
|
*/
|
|
7
8
|
|
|
@@ -11,71 +12,61 @@ const AGENTS = [
|
|
|
11
12
|
display_name: 'Claude Code',
|
|
12
13
|
skills_dir: '.claude/skills',
|
|
13
14
|
global_skills_dir: '.claude/skills',
|
|
14
|
-
detect_paths: ['.claude']
|
|
15
|
-
primary: true
|
|
15
|
+
detect_paths: ['.claude']
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
id: 'cursor',
|
|
19
19
|
display_name: 'Cursor',
|
|
20
20
|
skills_dir: '.cursor/skills',
|
|
21
21
|
global_skills_dir: '.cursor/skills',
|
|
22
|
-
detect_paths: ['.cursor']
|
|
23
|
-
primary: false
|
|
22
|
+
detect_paths: ['.cursor']
|
|
24
23
|
},
|
|
25
24
|
{
|
|
26
25
|
id: 'windsurf',
|
|
27
26
|
display_name: 'Windsurf',
|
|
28
27
|
skills_dir: '.windsurf/skills',
|
|
29
28
|
global_skills_dir: '.windsurf/skills',
|
|
30
|
-
detect_paths: ['.windsurf']
|
|
31
|
-
primary: false
|
|
29
|
+
detect_paths: ['.windsurf']
|
|
32
30
|
},
|
|
33
31
|
{
|
|
34
32
|
id: 'codex',
|
|
35
33
|
display_name: 'Codex',
|
|
36
34
|
skills_dir: '.codex/skills',
|
|
37
35
|
global_skills_dir: '.codex/skills',
|
|
38
|
-
detect_paths: ['.codex']
|
|
39
|
-
primary: false
|
|
36
|
+
detect_paths: ['.codex']
|
|
40
37
|
},
|
|
41
38
|
{
|
|
42
39
|
id: 'copilot',
|
|
43
40
|
display_name: 'GitHub Copilot',
|
|
44
41
|
skills_dir: '.github/skills',
|
|
45
42
|
global_skills_dir: '.github/skills',
|
|
46
|
-
detect_paths: ['.github/copilot']
|
|
47
|
-
primary: false
|
|
43
|
+
detect_paths: ['.github/copilot']
|
|
48
44
|
},
|
|
49
45
|
{
|
|
50
46
|
id: 'aider',
|
|
51
47
|
display_name: 'Aider',
|
|
52
48
|
skills_dir: '.aider/skills',
|
|
53
49
|
global_skills_dir: '.aider/skills',
|
|
54
|
-
detect_paths: ['.aider']
|
|
55
|
-
primary: false
|
|
50
|
+
detect_paths: ['.aider']
|
|
56
51
|
},
|
|
57
52
|
{
|
|
58
53
|
id: 'cline',
|
|
59
54
|
display_name: 'Cline',
|
|
60
55
|
skills_dir: '.cline/skills',
|
|
61
56
|
global_skills_dir: '.cline/skills',
|
|
62
|
-
detect_paths: ['.cline']
|
|
63
|
-
primary: false
|
|
57
|
+
detect_paths: ['.cline']
|
|
64
58
|
},
|
|
65
59
|
{
|
|
66
60
|
id: 'roo',
|
|
67
61
|
display_name: 'Roo Code',
|
|
68
62
|
skills_dir: '.roo/skills',
|
|
69
63
|
global_skills_dir: '.roo/skills',
|
|
70
|
-
detect_paths: ['.roo']
|
|
71
|
-
primary: false
|
|
64
|
+
detect_paths: ['.roo']
|
|
72
65
|
}
|
|
73
66
|
]
|
|
74
67
|
|
|
75
|
-
const PRIMARY_AGENT = AGENTS.find(a => a.primary)
|
|
76
|
-
|
|
77
68
|
const get_agent = (id) => AGENTS.find(a => a.id === id) || null
|
|
78
69
|
|
|
79
70
|
const get_all_agent_ids = () => AGENTS.map(a => a.id)
|
|
80
71
|
|
|
81
|
-
module.exports = { AGENTS,
|
|
72
|
+
module.exports = { AGENTS, get_agent, get_all_agent_ids }
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
|
+
const { read_config, set_config_value, delete_config_value, config_file_path } = require('../config/store')
|
|
3
|
+
const { AGENTS, get_agent } = require('../agents/registry')
|
|
4
|
+
const { detect_agents } = require('../agents/detector')
|
|
5
|
+
const { print_help, print_success, print_info, print_json, print_table } = require('../ui/output')
|
|
6
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
7
|
+
const { EXIT_CODES } = require('../constants')
|
|
8
|
+
const { green } = require('../ui/colors')
|
|
9
|
+
|
|
10
|
+
const HELP_TEXT = `Usage: happyskills config [key] [value] [options]
|
|
11
|
+
|
|
12
|
+
View or modify the global CLI configuration.
|
|
13
|
+
|
|
14
|
+
Subcommands:
|
|
15
|
+
happyskills config Show all config values
|
|
16
|
+
happyskills config agents Show current default agents
|
|
17
|
+
happyskills config agents <list> Set default agents (comma-separated IDs)
|
|
18
|
+
happyskills config agents --reset Reset to auto-detect (remove agents config)
|
|
19
|
+
happyskills config agents --list List all supported agents with status
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--list List all supported agents with detection and default status
|
|
23
|
+
--reset Remove the specified config key (reset to default)
|
|
24
|
+
--json Output as JSON
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
happyskills config
|
|
28
|
+
happyskills config agents
|
|
29
|
+
happyskills config agents claude,cursor
|
|
30
|
+
happyskills config agents --list
|
|
31
|
+
happyskills config agents --reset
|
|
32
|
+
happyskills config --json`
|
|
33
|
+
|
|
34
|
+
const _validate_agent_ids = (agents_str) => {
|
|
35
|
+
const ids = agents_str.split(',').map(s => s.trim()).filter(Boolean)
|
|
36
|
+
const valid = []
|
|
37
|
+
const unknown = []
|
|
38
|
+
|
|
39
|
+
for (const id of ids) {
|
|
40
|
+
const agent = get_agent(id)
|
|
41
|
+
if (agent) valid.push(id)
|
|
42
|
+
else unknown.push(id)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (unknown.length > 0) {
|
|
46
|
+
throw new UsageError(`Unknown agent(s): ${unknown.join(', ')}. Run 'happyskills config agents --list' to see available agents.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (valid.length === 0) {
|
|
50
|
+
throw new UsageError('At least one agent ID is required.')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return valid
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const _list_agents = async (config, is_json) => {
|
|
57
|
+
const [, detected] = await detect_agents()
|
|
58
|
+
const detected_ids = new Set((detected || []).map(a => a.id))
|
|
59
|
+
const config_agents = config.agents ? config.agents.split(',').map(s => s.trim()) : null
|
|
60
|
+
const default_ids = config_agents ? new Set(config_agents) : null
|
|
61
|
+
|
|
62
|
+
if (is_json) {
|
|
63
|
+
const agents = AGENTS.map(a => ({
|
|
64
|
+
id: a.id,
|
|
65
|
+
display_name: a.display_name,
|
|
66
|
+
skills_dir: a.skills_dir,
|
|
67
|
+
detected: detected_ids.has(a.id),
|
|
68
|
+
default: default_ids ? default_ids.has(a.id) : detected_ids.has(a.id)
|
|
69
|
+
}))
|
|
70
|
+
print_json({ data: { agents, source: default_ids ? 'config' : 'auto-detect' } })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rows = AGENTS.map(a => {
|
|
75
|
+
const is_detected = detected_ids.has(a.id)
|
|
76
|
+
const is_default = default_ids ? default_ids.has(a.id) : is_detected
|
|
77
|
+
return [
|
|
78
|
+
a.id,
|
|
79
|
+
a.display_name,
|
|
80
|
+
is_detected ? green('yes') : 'no',
|
|
81
|
+
is_default ? green('yes') : 'no'
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
print_table(['ID', 'Agent', 'Detected', 'Default'], rows)
|
|
86
|
+
console.log()
|
|
87
|
+
if (default_ids) {
|
|
88
|
+
print_info(`Default agents: ${config_agents.join(', ')} (from config)`)
|
|
89
|
+
} else {
|
|
90
|
+
print_info('Default agents: auto-detect (no config set)')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const run = (args) => catch_errors('Config failed', async () => {
|
|
95
|
+
if (args.flags._show_help) {
|
|
96
|
+
print_help(HELP_TEXT)
|
|
97
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const key = args._[0]
|
|
101
|
+
const value = args._[1]
|
|
102
|
+
const is_json = args.flags.json || false
|
|
103
|
+
const is_list = args.flags.list || false
|
|
104
|
+
const is_reset = args.flags.reset || false
|
|
105
|
+
|
|
106
|
+
const [config_err, config] = await read_config()
|
|
107
|
+
if (config_err) throw e('Failed to read config', config_err)
|
|
108
|
+
|
|
109
|
+
// No arguments — show full config
|
|
110
|
+
if (!key) {
|
|
111
|
+
if (is_json) {
|
|
112
|
+
print_json({ data: { config, path: config_file_path() } })
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const entries = Object.entries(config)
|
|
116
|
+
if (entries.length === 0) {
|
|
117
|
+
print_info(`No config set. Config file: ${config_file_path()}`)
|
|
118
|
+
} else {
|
|
119
|
+
for (const [k, v] of entries) {
|
|
120
|
+
console.log(`${k} = ${v}`)
|
|
121
|
+
}
|
|
122
|
+
console.log()
|
|
123
|
+
print_info(`Config file: ${config_file_path()}`)
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// agents subcommand
|
|
129
|
+
if (key === 'agents') {
|
|
130
|
+
// --list: show all agents with status
|
|
131
|
+
if (is_list) {
|
|
132
|
+
await _list_agents(config, is_json)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --reset: remove agents from config
|
|
137
|
+
if (is_reset) {
|
|
138
|
+
const [del_err] = await delete_config_value('agents')
|
|
139
|
+
if (del_err) throw e('Failed to reset agents config', del_err)
|
|
140
|
+
if (is_json) {
|
|
141
|
+
print_json({ data: { key: 'agents', status: 'reset', source: 'auto-detect' } })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
print_success('Default agents reset to auto-detect.')
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Set agents
|
|
149
|
+
if (value) {
|
|
150
|
+
const valid_ids = _validate_agent_ids(value)
|
|
151
|
+
const agents_str = valid_ids.join(',')
|
|
152
|
+
const [set_err] = await set_config_value('agents', agents_str)
|
|
153
|
+
if (set_err) throw e('Failed to set agents config', set_err)
|
|
154
|
+
if (is_json) {
|
|
155
|
+
print_json({ data: { key: 'agents', value: agents_str, agents: valid_ids } })
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
print_success(`Default agents set to: ${valid_ids.join(', ')}`)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get agents
|
|
163
|
+
const current = config.agents || null
|
|
164
|
+
if (is_json) {
|
|
165
|
+
print_json({ data: { key: 'agents', value: current, source: current ? 'config' : 'auto-detect' } })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (current) {
|
|
169
|
+
console.log(`agents = ${current}`)
|
|
170
|
+
} else {
|
|
171
|
+
print_info('No default agents configured. Using auto-detect.')
|
|
172
|
+
}
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Generic get/set for any key
|
|
177
|
+
if (is_reset) {
|
|
178
|
+
const [del_err] = await delete_config_value(key)
|
|
179
|
+
if (del_err) throw e(`Failed to reset ${key}`, del_err)
|
|
180
|
+
if (is_json) {
|
|
181
|
+
print_json({ data: { key, status: 'reset' } })
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
print_success(`Config key '${key}' reset.`)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (value) {
|
|
189
|
+
const [set_err] = await set_config_value(key, value)
|
|
190
|
+
if (set_err) throw e(`Failed to set ${key}`, set_err)
|
|
191
|
+
if (is_json) {
|
|
192
|
+
print_json({ data: { key, value } })
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
print_success(`${key} = ${value}`)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get
|
|
200
|
+
const current = config[key] !== undefined ? config[key] : null
|
|
201
|
+
if (is_json) {
|
|
202
|
+
print_json({ data: { key, value: current } })
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if (current !== null) {
|
|
206
|
+
console.log(`${key} = ${current}`)
|
|
207
|
+
} else {
|
|
208
|
+
print_info(`Config key '${key}' is not set.`)
|
|
209
|
+
}
|
|
210
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
211
|
+
|
|
212
|
+
module.exports = { run }
|
package/src/config/paths.js
CHANGED
|
@@ -14,9 +14,9 @@ const credentials_path = () => path.join(config_dir(), 'credentials.json')
|
|
|
14
14
|
|
|
15
15
|
const log_dir = () => path.join(config_dir(), 'logs')
|
|
16
16
|
|
|
17
|
-
const global_skills_dir = () => path.join(home_dir, '.
|
|
17
|
+
const global_skills_dir = () => path.join(home_dir, '.agents', 'skills')
|
|
18
18
|
|
|
19
|
-
const project_skills_dir = (project_root = process.cwd()) => path.join(project_root, '.
|
|
19
|
+
const project_skills_dir = (project_root = process.cwd()) => path.join(project_root, '.agents', 'skills')
|
|
20
20
|
|
|
21
21
|
const skills_dir = (global = false, project_root) => {
|
|
22
22
|
return global ? global_skills_dir() : project_skills_dir(project_root)
|
|
@@ -27,7 +27,7 @@ const tmp_dir = (base_skills_dir) => path.join(base_skills_dir, '.tmp')
|
|
|
27
27
|
const install_lock_path = (base_skills_dir) => path.join(base_skills_dir, '.install.lock')
|
|
28
28
|
|
|
29
29
|
const lock_root = (is_global, project_root) => {
|
|
30
|
-
return is_global ? path.join(home_dir, '.
|
|
30
|
+
return is_global ? path.join(home_dir, '.agents') : project_root
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const lock_file_path = (project_root = process.cwd()) => path.join(project_root, 'skills-lock.json')
|
package/src/config/paths.test.js
CHANGED
|
@@ -46,16 +46,16 @@ describe('paths', () => {
|
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
describe('global_skills_dir', () => {
|
|
49
|
-
it('returns path under home .
|
|
49
|
+
it('returns path under home .agents/skills', () => {
|
|
50
50
|
const result = paths.global_skills_dir()
|
|
51
|
-
assert.strictEqual(result, path.join(os.homedir(), '.
|
|
51
|
+
assert.strictEqual(result, path.join(os.homedir(), '.agents', 'skills'))
|
|
52
52
|
})
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
describe('project_skills_dir', () => {
|
|
56
56
|
it('returns path under project root', () => {
|
|
57
57
|
const result = paths.project_skills_dir('/my/project')
|
|
58
|
-
assert.strictEqual(result, path.join('/my/project', '.
|
|
58
|
+
assert.strictEqual(result, path.join('/my/project', '.agents', 'skills'))
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
61
|
|
|
@@ -109,10 +109,10 @@ describe('paths', () => {
|
|
|
109
109
|
}
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
it('does not walk up to a parent with .
|
|
112
|
+
it('does not walk up to a parent with .agents/skills', () => {
|
|
113
113
|
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
114
114
|
try {
|
|
115
|
-
fs.mkdirSync(path.join(tmp, '.
|
|
115
|
+
fs.mkdirSync(path.join(tmp, '.agents', 'skills'), { recursive: true })
|
|
116
116
|
const sub = path.join(tmp, 'sub')
|
|
117
117
|
fs.mkdirSync(sub, { recursive: true })
|
|
118
118
|
// should return sub, not tmp
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
3
|
+
const { config_dir } = require('./paths')
|
|
4
|
+
const { read_json, write_json, ensure_dir, file_exists } = require('../utils/fs')
|
|
5
|
+
|
|
6
|
+
const config_file_path = () => path.join(config_dir(), 'config.json')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read the global config file. Returns {} if the file does not exist.
|
|
10
|
+
*
|
|
11
|
+
* @returns {Promise} [errors, object]
|
|
12
|
+
*/
|
|
13
|
+
const read_config = () => catch_errors('Failed to read config', async () => {
|
|
14
|
+
const file = config_file_path()
|
|
15
|
+
const [, exists] = await file_exists(file)
|
|
16
|
+
if (!exists) return {}
|
|
17
|
+
const [err, data] = await read_json(file)
|
|
18
|
+
if (err) return {}
|
|
19
|
+
return data || {}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write the global config file. Ensures the config directory exists.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} data — full config object to write
|
|
26
|
+
* @returns {Promise} [errors]
|
|
27
|
+
*/
|
|
28
|
+
const write_config = (data) => catch_errors('Failed to write config', async () => {
|
|
29
|
+
const file = config_file_path()
|
|
30
|
+
await ensure_dir(path.dirname(file))
|
|
31
|
+
const [err] = await write_json(file, data)
|
|
32
|
+
if (err) throw err[0]
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a single config value by key.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} key
|
|
39
|
+
* @returns {Promise} [errors, any]
|
|
40
|
+
*/
|
|
41
|
+
const get_config_value = (key) => catch_errors('Failed to get config value', async () => {
|
|
42
|
+
const [err, config] = await read_config()
|
|
43
|
+
if (err) throw err[0]
|
|
44
|
+
return config[key] !== undefined ? config[key] : null
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set a single config value by key. Merges into existing config.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} key
|
|
51
|
+
* @param {any} value
|
|
52
|
+
* @returns {Promise} [errors]
|
|
53
|
+
*/
|
|
54
|
+
const set_config_value = (key, value) => catch_errors('Failed to set config value', async () => {
|
|
55
|
+
const [err, config] = await read_config()
|
|
56
|
+
if (err) throw err[0]
|
|
57
|
+
config[key] = value
|
|
58
|
+
const [write_err] = await write_config(config)
|
|
59
|
+
if (write_err) throw write_err[0]
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Delete a single config value by key.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} key
|
|
66
|
+
* @returns {Promise} [errors]
|
|
67
|
+
*/
|
|
68
|
+
const delete_config_value = (key) => catch_errors('Failed to delete config value', async () => {
|
|
69
|
+
const [err, config] = await read_config()
|
|
70
|
+
if (err) throw err[0]
|
|
71
|
+
delete config[key]
|
|
72
|
+
const [write_err] = await write_config(config)
|
|
73
|
+
if (write_err) throw write_err[0]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
module.exports = { config_file_path, read_config, write_config, get_config_value, set_config_value, delete_config_value }
|
package/src/constants.js
CHANGED
package/src/engine/installer.js
CHANGED
|
@@ -24,10 +24,10 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
24
24
|
const { version, global: is_global = false, force = false, fresh = false, project_root, agents: agents_flag } = options
|
|
25
25
|
const base_dir = skills_dir(is_global, project_root)
|
|
26
26
|
|
|
27
|
-
// Resolve target agents
|
|
27
|
+
// Resolve target agents
|
|
28
28
|
const [agents_err, agents_result] = await resolve_agents(agents_flag)
|
|
29
29
|
if (agents_err) throw e('Agent resolution failed', agents_err)
|
|
30
|
-
const {
|
|
30
|
+
const { agents } = agents_result
|
|
31
31
|
const temp_dir = tmp_dir(base_dir)
|
|
32
32
|
const lock_dir = lock_root(is_global, project_root)
|
|
33
33
|
|
|
@@ -136,13 +136,13 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
136
136
|
await fs.promises.rename(tmp_path, final_dir)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Link to
|
|
140
|
-
if (
|
|
141
|
-
spinner.update(`Linking to ${
|
|
139
|
+
// Link to detected agents (non-fatal — warnings only)
|
|
140
|
+
if (agents.length > 0) {
|
|
141
|
+
spinner.update(`Linking to ${agents.length} agent(s)...`)
|
|
142
142
|
for (const { pkg } of downloaded) {
|
|
143
143
|
const name = pkg.skill.split('/')[1]
|
|
144
144
|
const source = skill_install_dir(base_dir, name)
|
|
145
|
-
const [link_errs] = await link_to_agents(source,
|
|
145
|
+
const [link_errs] = await link_to_agents(source, agents, { global: is_global, project_root, skill_name: name })
|
|
146
146
|
if (link_errs) {
|
|
147
147
|
print_warn(`Warning: failed to link ${pkg.skill} to some agents`)
|
|
148
148
|
}
|
|
@@ -181,8 +181,8 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
181
181
|
|
|
182
182
|
spinner.succeed(`Installed ${packages_to_install.length} package(s)`)
|
|
183
183
|
|
|
184
|
-
if (
|
|
185
|
-
print_info(`Linked to: ${
|
|
184
|
+
if (agents.length > 0) {
|
|
185
|
+
print_info(`Linked to: ${agents.map(a => a.display_name).join(', ')}`)
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
const [, missing_deps] = await check_system_dependencies(packages)
|
|
@@ -201,7 +201,7 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
|
|
|
201
201
|
const warnings = _format_warnings(missing_deps)
|
|
202
202
|
const forced = forced_pkgs.map(p => ({ skill: p.skill, version: p.version }))
|
|
203
203
|
|
|
204
|
-
const linked_agents =
|
|
204
|
+
const linked_agents = agents.map(a => a.id)
|
|
205
205
|
return { skill, version: packages[0]?.version, no_op: false, installed, skipped, warnings, forced, packages: packages_to_install.length, linked_agents }
|
|
206
206
|
} catch (err) {
|
|
207
207
|
await remove_dir(temp_dir)
|
|
@@ -25,7 +25,7 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
|
|
|
25
25
|
// Resolve target agents
|
|
26
26
|
const [agents_err, agents_result] = await resolve_agents(agents_flag)
|
|
27
27
|
if (agents_err) throw e('Agent resolution failed', agents_err)
|
|
28
|
-
const {
|
|
28
|
+
const { agents } = agents_result
|
|
29
29
|
const base_dir = skills_dir(is_global, project_root)
|
|
30
30
|
const lock_dir = lock_root(is_global, project_root)
|
|
31
31
|
|
|
@@ -56,10 +56,10 @@ const uninstall = (skill, options = {}) => catch_errors('Uninstall failed', asyn
|
|
|
56
56
|
if (rm_errors) throw e(`Failed to remove ${name}`, rm_errors)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// Unlink from
|
|
60
|
-
if (
|
|
59
|
+
// Unlink from detected agents
|
|
60
|
+
if (agents.length > 0) {
|
|
61
61
|
for (const name of to_remove) {
|
|
62
|
-
await unlink_from_agents(name.split('/')[1],
|
|
62
|
+
await unlink_from_agents(name.split('/')[1], agents, { global: is_global, project_root })
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -315,8 +315,8 @@ describe('CLI — --json: success responses use { data } envelope', () => {
|
|
|
315
315
|
it('init --json error when skill.json already exists returns { error }', () => {
|
|
316
316
|
const tmp = make_tmp()
|
|
317
317
|
try {
|
|
318
|
-
// Create .
|
|
319
|
-
const skill_dir = path.join(tmp, '.
|
|
318
|
+
// Create .agents/skills/test-skill/skill.json first
|
|
319
|
+
const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
|
|
320
320
|
fs.mkdirSync(skill_dir, { recursive: true })
|
|
321
321
|
fs.writeFileSync(path.join(skill_dir, 'skill.json'), '{}')
|
|
322
322
|
const { stdout, code } = run(['init', 'test-skill', '--json'], {}, { cwd: tmp })
|
|
@@ -533,7 +533,7 @@ describe('CLI — validate command', () => {
|
|
|
533
533
|
|
|
534
534
|
it('exits 0 for a valid skill and reports all checks passed', () => {
|
|
535
535
|
const tmp = make_tmp()
|
|
536
|
-
const skill_dir = path.join(tmp, '.
|
|
536
|
+
const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
|
|
537
537
|
fs.mkdirSync(skill_dir, { recursive: true })
|
|
538
538
|
fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: test-skill\ndescription: A valid test skill for unit testing\n---\n\n# Test Skill\n')
|
|
539
539
|
fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({
|
|
@@ -551,7 +551,7 @@ describe('CLI — validate command', () => {
|
|
|
551
551
|
|
|
552
552
|
it('exits 1 for an invalid skill', () => {
|
|
553
553
|
const tmp = make_tmp()
|
|
554
|
-
const skill_dir = path.join(tmp, '.
|
|
554
|
+
const skill_dir = path.join(tmp, '.agents', 'skills', 'bad-skill')
|
|
555
555
|
fs.mkdirSync(skill_dir, { recursive: true })
|
|
556
556
|
fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: bad-skill\n---\n')
|
|
557
557
|
fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({ name: 'bad-skill' }, null, '\t'))
|
|
@@ -565,7 +565,7 @@ describe('CLI — validate command', () => {
|
|
|
565
565
|
|
|
566
566
|
it('--json returns correct schema for a valid skill', () => {
|
|
567
567
|
const tmp = make_tmp()
|
|
568
|
-
const skill_dir = path.join(tmp, '.
|
|
568
|
+
const skill_dir = path.join(tmp, '.agents', 'skills', 'test-skill')
|
|
569
569
|
fs.mkdirSync(skill_dir, { recursive: true })
|
|
570
570
|
fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: test-skill\ndescription: A valid test skill for unit testing\n---\n\n# Test\n')
|
|
571
571
|
fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({
|
|
@@ -592,7 +592,7 @@ describe('CLI — validate command', () => {
|
|
|
592
592
|
|
|
593
593
|
it('--json returns errors for an invalid skill', () => {
|
|
594
594
|
const tmp = make_tmp()
|
|
595
|
-
const skill_dir = path.join(tmp, '.
|
|
595
|
+
const skill_dir = path.join(tmp, '.agents', 'skills', 'bad-skill')
|
|
596
596
|
fs.mkdirSync(skill_dir, { recursive: true })
|
|
597
597
|
fs.writeFileSync(path.join(skill_dir, 'SKILL.md'), '---\nname: bad-skill\n---\n')
|
|
598
598
|
fs.writeFileSync(path.join(skill_dir, 'skill.json'), JSON.stringify({ name: 'bad-skill' }, null, '\t'))
|