happyskills 0.24.0 → 0.26.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 +21 -0
- package/package.json +1 -1
- package/src/api/group_permissions.js +32 -0
- package/src/api/groups.js +58 -0
- package/src/api/members.js +28 -0
- package/src/api/users.js +11 -0
- package/src/commands/access.js +212 -0
- package/src/commands/convert.js +43 -14
- package/src/commands/groups.js +219 -0
- package/src/commands/init.js +28 -8
- package/src/commands/people.js +162 -0
- package/src/constants.js +6 -2
- package/src/index.js +3 -0
- package/src/utils/prompt.js +16 -0
- package/src/validation/skill_md_rules.js +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.26.0] - 2026-04-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `people` command with subcommands: `list`, `add`, `remove`, `role`, `search` for workspace membership management
|
|
14
|
+
- Add `groups` command (alias: `grp`) with subcommands: `list`, `create`, `delete`, `show`, `add`, `remove`, `default` for workspace group management
|
|
15
|
+
- Add `access` command with subcommands: `list`, `grant`, `revoke`, `set` for group-level skill permission management
|
|
16
|
+
- Add `confirm_prompt` utility for interactive confirmation on destructive operations (`people remove`, `groups delete`)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Make `convert` authentication optional — when not logged in, `--workspace <slug>` is required and the registry conflict check is skipped
|
|
20
|
+
|
|
21
|
+
## [0.25.0] - 2026-03-30
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Add `--agents` flag to `init` and `convert` commands for explicit agent targeting, matching `install` behavior
|
|
25
|
+
- Add `recommendations` array to `validate` description max_length error — provides a 4-step procedure (audit, lossless compression, lossy compression, verify) for safely shortening descriptions without breaking auto-invocation triggers
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Fix `init` not creating symlinks to secondary agents (Cursor, Windsurf, Codex, etc.) — newly scaffolded skills now use the same multi-agent linking as `install`
|
|
29
|
+
- Fix `convert` not creating symlinks to secondary agents — converted skills now get symlinked to all detected agents
|
|
30
|
+
|
|
10
31
|
## [0.24.0] - 2026-03-30
|
|
11
32
|
|
|
12
33
|
### Added
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const client = require('./client')
|
|
3
|
+
|
|
4
|
+
const repo_path = (owner, name) => `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/groups`
|
|
5
|
+
|
|
6
|
+
const list_by_skill = (owner, name) => catch_errors('List skill groups failed', async () => {
|
|
7
|
+
const [errors, data] = await client.get(repo_path(owner, name))
|
|
8
|
+
if (errors) throw errors[errors.length - 1]
|
|
9
|
+
return data
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const grant = (owner, name, body) => catch_errors('Grant access failed', async () => {
|
|
13
|
+
const [errors, data] = await client.post(repo_path(owner, name), body)
|
|
14
|
+
if (errors) throw errors[errors.length - 1]
|
|
15
|
+
return data
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const revoke = (owner, name, group_id, revoke_dependencies = true) => catch_errors('Revoke access failed', async () => {
|
|
19
|
+
const path = `${repo_path(owner, name)}/${encodeURIComponent(group_id)}?revoke_dependencies=${revoke_dependencies}`
|
|
20
|
+
const [errors, data] = await client.del(path)
|
|
21
|
+
if (errors) throw errors[errors.length - 1]
|
|
22
|
+
return data
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const update = (owner, name, group_id, permission) => catch_errors('Update permission failed', async () => {
|
|
26
|
+
const path = `${repo_path(owner, name)}/${encodeURIComponent(group_id)}`
|
|
27
|
+
const [errors, data] = await client.patch(path, { permission })
|
|
28
|
+
if (errors) throw errors[errors.length - 1]
|
|
29
|
+
return data
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
module.exports = { list_by_skill, grant, revoke, update }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const client = require('./client')
|
|
3
|
+
|
|
4
|
+
const ws_path = (slug) => `/workspaces/${encodeURIComponent(slug)}/groups`
|
|
5
|
+
const grp_path = (slug, name) => `${ws_path(slug)}/${encodeURIComponent(name)}`
|
|
6
|
+
|
|
7
|
+
const list_groups = (slug) => catch_errors('List groups failed', async () => {
|
|
8
|
+
const [errors, data] = await client.get(ws_path(slug))
|
|
9
|
+
if (errors) throw errors[errors.length - 1]
|
|
10
|
+
return data
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const create_group = (slug, name, description, is_default = false) => catch_errors('Create group failed', async () => {
|
|
14
|
+
const body = { name }
|
|
15
|
+
if (description) body.description = description
|
|
16
|
+
if (is_default) body.is_default = true
|
|
17
|
+
const [errors, data] = await client.post(ws_path(slug), body)
|
|
18
|
+
if (errors) throw errors[errors.length - 1]
|
|
19
|
+
return data
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const delete_group = (slug, name) => catch_errors('Delete group failed', async () => {
|
|
23
|
+
const [errors, data] = await client.del(grp_path(slug, name))
|
|
24
|
+
if (errors) throw errors[errors.length - 1]
|
|
25
|
+
return data
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const show_group = (slug, name) => catch_errors('Show group failed', async () => {
|
|
29
|
+
const [errors, data] = await client.get(grp_path(slug, name))
|
|
30
|
+
if (errors) throw errors[errors.length - 1]
|
|
31
|
+
return data
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const add_member = (slug, group, username) => catch_errors('Add group member failed', async () => {
|
|
35
|
+
const [errors, data] = await client.post(`${grp_path(slug, group)}/members`, { username })
|
|
36
|
+
if (errors) throw errors[errors.length - 1]
|
|
37
|
+
return data
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const remove_member = (slug, group, username) => catch_errors('Remove group member failed', async () => {
|
|
41
|
+
const [errors, data] = await client.del(`${grp_path(slug, group)}/members/${encodeURIComponent(username)}`)
|
|
42
|
+
if (errors) throw errors[errors.length - 1]
|
|
43
|
+
return data
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const update_group = (slug, name, fields) => catch_errors('Update group failed', async () => {
|
|
47
|
+
const [errors, data] = await client.patch(grp_path(slug, name), fields)
|
|
48
|
+
if (errors) throw errors[errors.length - 1]
|
|
49
|
+
return data
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const list_permissions = (slug, group) => catch_errors('List group permissions failed', async () => {
|
|
53
|
+
const [errors, data] = await client.get(`${grp_path(slug, group)}/permissions`)
|
|
54
|
+
if (errors) throw errors[errors.length - 1]
|
|
55
|
+
return data
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
module.exports = { list_groups, create_group, delete_group, show_group, add_member, remove_member, update_group, list_permissions }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const client = require('./client')
|
|
3
|
+
|
|
4
|
+
const list_members = (slug) => catch_errors('List members failed', async () => {
|
|
5
|
+
const [errors, data] = await client.get(`/workspaces/${encodeURIComponent(slug)}/members`)
|
|
6
|
+
if (errors) throw errors[errors.length - 1]
|
|
7
|
+
return data
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const add_member = (slug, username, role = 'member') => catch_errors('Add member failed', async () => {
|
|
11
|
+
const [errors, data] = await client.post(`/workspaces/${encodeURIComponent(slug)}/members`, { username, role })
|
|
12
|
+
if (errors) throw errors[errors.length - 1]
|
|
13
|
+
return data
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const remove_member = (slug, username) => catch_errors('Remove member failed', async () => {
|
|
17
|
+
const [errors, data] = await client.del(`/workspaces/${encodeURIComponent(slug)}/members/${encodeURIComponent(username)}`)
|
|
18
|
+
if (errors) throw errors[errors.length - 1]
|
|
19
|
+
return data
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const update_role = (slug, username, role) => catch_errors('Update role failed', async () => {
|
|
23
|
+
const [errors, data] = await client.patch(`/workspaces/${encodeURIComponent(slug)}/members/${encodeURIComponent(username)}`, { role })
|
|
24
|
+
if (errors) throw errors[errors.length - 1]
|
|
25
|
+
return data
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
module.exports = { list_members, add_member, remove_member, update_role }
|
package/src/api/users.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const client = require('./client')
|
|
3
|
+
|
|
4
|
+
const search_users = (query, limit = 10) => catch_errors('Search users failed', async () => {
|
|
5
|
+
const params = new URLSearchParams({ q: query, limit: String(limit), exclude_self: 'true' })
|
|
6
|
+
const [errors, data] = await client.get(`/users/search?${params}`)
|
|
7
|
+
if (errors) throw errors[errors.length - 1]
|
|
8
|
+
return data
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
module.exports = { search_users }
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const { require_token } = require('../auth/token_store')
|
|
3
|
+
const groups_api = require('../api/groups')
|
|
4
|
+
const perms_api = require('../api/group_permissions')
|
|
5
|
+
const { print_help, print_json, print_table, print_success, print_warn } = require('../ui/output')
|
|
6
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
7
|
+
const { EXIT_CODES } = require('../constants')
|
|
8
|
+
|
|
9
|
+
const HELP_TEXT = `Usage: happyskills access <subcommand> [options]
|
|
10
|
+
|
|
11
|
+
Manage group-level skill access.
|
|
12
|
+
|
|
13
|
+
Subcommands:
|
|
14
|
+
list List access grants (by group or by skill)
|
|
15
|
+
grant Grant a group access to a skill
|
|
16
|
+
revoke Revoke a group's access to a skill
|
|
17
|
+
set Change a group's permission on a skill
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
-w, --workspace <slug> Workspace slug (required for --group list mode)
|
|
21
|
+
--group <group> Group name
|
|
22
|
+
--skill <owner/name> Skill identifier (owner/name)
|
|
23
|
+
--permission <perm> Permission: read, write, or admin
|
|
24
|
+
--confirm Confirm dependency cascade for grant
|
|
25
|
+
--dep-permission <perm> Permission for dependencies: read or write
|
|
26
|
+
--keep-deps Keep dependency grants on revoke
|
|
27
|
+
--json Output as JSON
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
happyskills access list -w acme --group engineering
|
|
31
|
+
happyskills access list --skill acme/deploy-aws
|
|
32
|
+
happyskills access grant --skill acme/deploy-aws --group engineering --permission read
|
|
33
|
+
happyskills access revoke --skill acme/deploy-aws --group engineering
|
|
34
|
+
happyskills access set --skill acme/deploy-aws --group engineering --permission write`
|
|
35
|
+
|
|
36
|
+
const parse_skill = (skill_flag) => {
|
|
37
|
+
if (!skill_flag) throw new UsageError('--skill <owner/name> is required')
|
|
38
|
+
const parts = skill_flag.split('/')
|
|
39
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) throw new UsageError('--skill must be in owner/name format')
|
|
40
|
+
return { owner: parts[0], name: parts[1] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolve_group_id = (groups, group_name) => {
|
|
44
|
+
const match = groups.find(g => g.group_name === group_name || g.name === group_name)
|
|
45
|
+
if (!match) throw new UsageError(`Group "${group_name}" not found in skill's access list`)
|
|
46
|
+
return match.id || match.group_id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const list_access = (args) => catch_errors('List access failed', async () => {
|
|
50
|
+
const group_flag = args.flags.group
|
|
51
|
+
const skill_flag = args.flags.skill
|
|
52
|
+
|
|
53
|
+
if (!group_flag && !skill_flag) {
|
|
54
|
+
throw new UsageError('Either --group or --skill is required. Run happyskills access list --help')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (group_flag) {
|
|
58
|
+
const workspace = args.flags.workspace
|
|
59
|
+
if (!workspace) throw new UsageError('--workspace (-w) is required when using --group')
|
|
60
|
+
|
|
61
|
+
const [errors, permissions] = await groups_api.list_permissions(workspace, group_flag)
|
|
62
|
+
if (errors) throw errors[errors.length - 1]
|
|
63
|
+
|
|
64
|
+
if (args.flags.json) {
|
|
65
|
+
print_json({ data: permissions })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!permissions || permissions.length === 0) {
|
|
70
|
+
console.log('No skill access grants found.')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
print_table(
|
|
75
|
+
['Skill', 'Permission', 'Direct'],
|
|
76
|
+
permissions.map(p => [p.skill || `${p.owner}/${p.name}`, p.permission, p.is_direct !== false ? 'yes' : 'no'])
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { owner, name } = parse_skill(skill_flag)
|
|
82
|
+
const [errors, groups] = await perms_api.list_by_skill(owner, name)
|
|
83
|
+
if (errors) throw errors[errors.length - 1]
|
|
84
|
+
|
|
85
|
+
if (args.flags.json) {
|
|
86
|
+
print_json({ data: groups })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!groups || groups.length === 0) {
|
|
91
|
+
console.log('No group access grants found.')
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
print_table(
|
|
96
|
+
['Group', 'Permission', 'Direct'],
|
|
97
|
+
groups.map(g => [g.group_name || g.name, g.permission, g.is_direct !== false ? 'yes' : 'no'])
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const grant_access = (args) => catch_errors('Grant access failed', async () => {
|
|
102
|
+
const { owner, name } = parse_skill(args.flags.skill)
|
|
103
|
+
const group_name = args.flags.group
|
|
104
|
+
if (!group_name) throw new UsageError('--group is required')
|
|
105
|
+
const permission = args.flags.permission
|
|
106
|
+
if (!permission) throw new UsageError('--permission is required (read, write, or admin)')
|
|
107
|
+
|
|
108
|
+
const body = { group_name, permission }
|
|
109
|
+
if (args.flags.confirm) body.confirm = true
|
|
110
|
+
if (args.flags['dep-permission']) body.dependency_permission = args.flags['dep-permission']
|
|
111
|
+
|
|
112
|
+
const [errors, data] = await perms_api.grant(owner, name, body)
|
|
113
|
+
if (errors) throw errors[errors.length - 1]
|
|
114
|
+
|
|
115
|
+
if (data && data.requires_confirmation) {
|
|
116
|
+
if (args.flags.json) {
|
|
117
|
+
print_json({ data })
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
print_warn('This skill has private dependencies that will also receive access:')
|
|
122
|
+
const deps = data.dependencies || []
|
|
123
|
+
for (const dep of deps) {
|
|
124
|
+
console.log(` - ${dep.owner}/${dep.name}`)
|
|
125
|
+
}
|
|
126
|
+
console.log()
|
|
127
|
+
console.log('Re-run with --confirm to proceed, or add --dep-permission <read|write> to set dependency permission level.')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (args.flags.json) {
|
|
132
|
+
print_json({ data })
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
print_success(`Granted ${group_name} ${permission} access to ${owner}/${name}`)
|
|
137
|
+
const dep_count = data?.dependency_grants?.length || 0
|
|
138
|
+
if (dep_count > 0) {
|
|
139
|
+
console.log(` Also granted access to ${dep_count} dependenc${dep_count === 1 ? 'y' : 'ies'}`)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const revoke_access = (args) => catch_errors('Revoke access failed', async () => {
|
|
144
|
+
const { owner, name } = parse_skill(args.flags.skill)
|
|
145
|
+
const group_name = args.flags.group
|
|
146
|
+
if (!group_name) throw new UsageError('--group is required')
|
|
147
|
+
|
|
148
|
+
const revoke_deps = args.flags['keep-deps'] !== true
|
|
149
|
+
|
|
150
|
+
// Resolve group name to group ID
|
|
151
|
+
const [list_errors, groups] = await perms_api.list_by_skill(owner, name)
|
|
152
|
+
if (list_errors) throw list_errors[list_errors.length - 1]
|
|
153
|
+
|
|
154
|
+
const group_id = resolve_group_id(groups, group_name)
|
|
155
|
+
|
|
156
|
+
const [errors, data] = await perms_api.revoke(owner, name, group_id, revoke_deps)
|
|
157
|
+
if (errors) throw errors[errors.length - 1]
|
|
158
|
+
|
|
159
|
+
if (args.flags.json) {
|
|
160
|
+
print_json({ data })
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
print_success(`Revoked ${group_name} access to ${owner}/${name}`)
|
|
165
|
+
const dep_count = data?.dependencies_revoked || 0
|
|
166
|
+
if (dep_count > 0) {
|
|
167
|
+
console.log(` Also revoked access to ${dep_count} dependenc${dep_count === 1 ? 'y' : 'ies'}`)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const set_permission = (args) => catch_errors('Set permission failed', async () => {
|
|
172
|
+
const { owner, name } = parse_skill(args.flags.skill)
|
|
173
|
+
const group_name = args.flags.group
|
|
174
|
+
if (!group_name) throw new UsageError('--group is required')
|
|
175
|
+
const permission = args.flags.permission
|
|
176
|
+
if (!permission) throw new UsageError('--permission is required (read, write, or admin)')
|
|
177
|
+
|
|
178
|
+
// Resolve group name to group ID
|
|
179
|
+
const [list_errors, groups] = await perms_api.list_by_skill(owner, name)
|
|
180
|
+
if (list_errors) throw list_errors[list_errors.length - 1]
|
|
181
|
+
|
|
182
|
+
const group_id = resolve_group_id(groups, group_name)
|
|
183
|
+
|
|
184
|
+
const [errors, data] = await perms_api.update(owner, name, group_id, permission)
|
|
185
|
+
if (errors) throw errors[errors.length - 1]
|
|
186
|
+
|
|
187
|
+
if (args.flags.json) {
|
|
188
|
+
print_json({ data })
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
print_success(`Changed ${group_name} permission on ${owner}/${name} to ${permission}`)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const run = (args) => catch_errors('Access command failed', async () => {
|
|
196
|
+
if (args.flags._show_help) {
|
|
197
|
+
print_help(HELP_TEXT)
|
|
198
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await require_token()
|
|
202
|
+
|
|
203
|
+
const sub = args._[0]
|
|
204
|
+
if (!sub || sub === 'list') return list_access(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
205
|
+
if (sub === 'grant') return grant_access(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
206
|
+
if (sub === 'revoke') return revoke_access(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
207
|
+
if (sub === 'set') return set_permission(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
208
|
+
|
|
209
|
+
throw new UsageError(`Unknown subcommand: ${sub}. Run happyskills access --help`)
|
|
210
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
211
|
+
|
|
212
|
+
module.exports = { run }
|
package/src/commands/convert.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const readline = require('readline')
|
|
4
4
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
5
|
-
const {
|
|
5
|
+
const { load_token } = require('../auth/token_store')
|
|
6
6
|
const repos_api = require('../api/repos')
|
|
7
7
|
const workspaces_api = require('../api/workspaces')
|
|
8
8
|
const { parse_frontmatter } = require('../utils/skill_scanner')
|
|
@@ -13,6 +13,7 @@ const { write_lock, update_lock_skills } = require('../lock/writer')
|
|
|
13
13
|
const { hash_directory } = require('../lock/integrity')
|
|
14
14
|
const { skills_dir, find_project_root, lock_root } = require('../config/paths')
|
|
15
15
|
const { file_exists } = require('../utils/fs')
|
|
16
|
+
const { resolve_agents, link_to_agents } = require('../agents')
|
|
16
17
|
const { create_spinner } = require('../ui/spinner')
|
|
17
18
|
const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
|
|
18
19
|
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
@@ -26,10 +27,12 @@ Arguments:
|
|
|
26
27
|
skill-name Name of the skill in .claude/skills/
|
|
27
28
|
|
|
28
29
|
Options:
|
|
29
|
-
-g, --global Look in global skills (~/.
|
|
30
|
+
-g, --global Look in global skills (~/.agents/skills/)
|
|
30
31
|
--workspace <slug> Target workspace (if you have multiple)
|
|
31
32
|
--version <ver> Initial version (default: 1.0.0)
|
|
32
33
|
--keywords <tags> Comma-separated keywords (e.g., "deploy,aws,iac")
|
|
34
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
35
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
33
36
|
-y, --yes Skip confirmation prompt
|
|
34
37
|
|
|
35
38
|
Examples:
|
|
@@ -69,7 +72,8 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
69
72
|
throw new UsageError('Please specify a skill name (e.g., happyskills convert my-skill).')
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
await
|
|
75
|
+
const [, token_data] = await load_token()
|
|
76
|
+
const is_authenticated = !!token_data
|
|
73
77
|
|
|
74
78
|
const is_global = args.flags.global || false
|
|
75
79
|
const project_root = find_project_root()
|
|
@@ -119,22 +123,29 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
let workspace
|
|
127
|
+
if (is_authenticated) {
|
|
128
|
+
const spinner = create_spinner('Resolving workspace...')
|
|
129
|
+
const [ws_err, workspaces] = await workspaces_api.list_workspaces()
|
|
130
|
+
if (ws_err) { spinner.fail('Failed to list workspaces'); throw e('Failed to list workspaces', ws_err) }
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
if (ws_err) { spinner.fail('Failed to list workspaces'); throw e('Failed to list workspaces', ws_err) }
|
|
132
|
+
workspace = choose_workspace(workspaces, args.flags.workspace)
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
spinner.update('Checking registry...')
|
|
135
|
+
const [repo_err] = await repos_api.get_repo(workspace.slug, skill_name, { auth: true })
|
|
136
|
+
if (!repo_err) {
|
|
137
|
+
spinner.stop()
|
|
138
|
+
throw new CliError(`Skill "${workspace.slug}/${skill_name}" already exists in the registry. Use \`happyskills publish\` instead to push a new version.`, EXIT_CODES.ERROR)
|
|
139
|
+
}
|
|
132
140
|
spinner.stop()
|
|
133
|
-
|
|
141
|
+
} else {
|
|
142
|
+
if (!args.flags.workspace) {
|
|
143
|
+
throw new UsageError('Not logged in. Either run `happyskills login` first, or pass --workspace <slug> to convert without authentication.')
|
|
144
|
+
}
|
|
145
|
+
workspace = { slug: args.flags.workspace }
|
|
146
|
+
print_warn('Not authenticated — skipping registry conflict check. Run `happyskills login` before publishing.')
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
spinner.stop()
|
|
137
|
-
|
|
138
149
|
if (!args.flags.yes && !args.flags.json) {
|
|
139
150
|
console.error('')
|
|
140
151
|
console.error(` Skill: ${skill_name}`)
|
|
@@ -190,8 +201,25 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
190
201
|
const [lock_err] = await write_lock(lock_dir, new_skills)
|
|
191
202
|
if (lock_err) { pub_spinner.fail('Failed to write lock file'); throw e('Lock write failed', lock_err) }
|
|
192
203
|
|
|
204
|
+
// Link to detected agents (non-fatal — warnings only)
|
|
205
|
+
const linked_agents = []
|
|
206
|
+
const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
|
|
207
|
+
if (!agents_err && agents_result?.agents?.length > 0) {
|
|
208
|
+
pub_spinner.update(`Linking to ${agents_result.agents.length} agent(s)...`)
|
|
209
|
+
const [link_errs] = await link_to_agents(skill_dir, agents_result.agents, { global: is_global, project_root, skill_name })
|
|
210
|
+
if (link_errs) {
|
|
211
|
+
print_warn(`Warning: failed to link ${skill_name} to some agents`)
|
|
212
|
+
} else {
|
|
213
|
+
linked_agents.push(...agents_result.agents.map(a => a.id))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
193
217
|
pub_spinner.succeed(`Converted ${full_name}@${merged_version}`)
|
|
194
218
|
|
|
219
|
+
if (linked_agents.length > 0) {
|
|
220
|
+
print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
195
223
|
if (args.flags.json) {
|
|
196
224
|
const json_data = {
|
|
197
225
|
skill: full_name,
|
|
@@ -199,6 +227,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
199
227
|
workspace: workspace.slug,
|
|
200
228
|
description: merged_description || ''
|
|
201
229
|
}
|
|
230
|
+
if (linked_agents.length > 0) json_data.linked_agents = linked_agents
|
|
202
231
|
if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
|
|
203
232
|
print_json({ data: json_data })
|
|
204
233
|
return
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const { require_token } = require('../auth/token_store')
|
|
3
|
+
const groups_api = require('../api/groups')
|
|
4
|
+
const { print_help, print_json, print_table, print_success, print_label, print_hint } = require('../ui/output')
|
|
5
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
6
|
+
const { EXIT_CODES } = require('../constants')
|
|
7
|
+
|
|
8
|
+
const HELP_TEXT = `Usage: happyskills groups <subcommand> [options]
|
|
9
|
+
|
|
10
|
+
Manage workspace groups and group membership.
|
|
11
|
+
|
|
12
|
+
Subcommands:
|
|
13
|
+
list List workspace groups
|
|
14
|
+
create <name> Create a group
|
|
15
|
+
delete <name> Delete a group
|
|
16
|
+
show <name> Show group detail and members
|
|
17
|
+
add <group> <username> Add a person to a group
|
|
18
|
+
remove <group> <username> Remove a person from a group
|
|
19
|
+
default <name> --on|--off Toggle default group status
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-w, --workspace <slug> Workspace slug (required)
|
|
23
|
+
--description <desc> Description for create
|
|
24
|
+
--default Mark as default group on create
|
|
25
|
+
--on Enable default status
|
|
26
|
+
--off Disable default status
|
|
27
|
+
-y, --yes Skip confirmation prompts
|
|
28
|
+
--json Output as JSON
|
|
29
|
+
|
|
30
|
+
Aliases:
|
|
31
|
+
grp Alias for groups
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
happyskills groups list -w acme
|
|
35
|
+
happyskills groups create -w acme engineering --description "Core team"
|
|
36
|
+
happyskills groups show -w acme engineering
|
|
37
|
+
happyskills groups add -w acme engineering alice
|
|
38
|
+
happyskills groups delete -w acme engineering --yes
|
|
39
|
+
happyskills grp default -w acme engineering --on`
|
|
40
|
+
|
|
41
|
+
const require_workspace = (args) => {
|
|
42
|
+
const workspace = args.flags.workspace
|
|
43
|
+
if (!workspace) throw new UsageError('--workspace (-w) is required')
|
|
44
|
+
return workspace
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const list_groups_cmd = (args) => catch_errors('List groups failed', async () => {
|
|
48
|
+
const workspace = require_workspace(args)
|
|
49
|
+
const [errors, groups] = await groups_api.list_groups(workspace)
|
|
50
|
+
if (errors) throw errors[errors.length - 1]
|
|
51
|
+
|
|
52
|
+
if (args.flags.json) {
|
|
53
|
+
print_json({ data: groups })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!groups || groups.length === 0) {
|
|
58
|
+
console.log('No groups found.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
print_table(
|
|
63
|
+
['Name', 'Description', 'Members', 'Default'],
|
|
64
|
+
groups.map(g => [g.name, g.description || '', g.member_count ?? '', g.is_default ? 'yes' : 'no'])
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const create_group = (args) => catch_errors('Create group failed', async () => {
|
|
69
|
+
const workspace = require_workspace(args)
|
|
70
|
+
const name = args._[1]
|
|
71
|
+
if (!name) throw new UsageError('Group name is required. Usage: happyskills groups create -w <workspace> <name>')
|
|
72
|
+
|
|
73
|
+
const description = args.flags.description || null
|
|
74
|
+
const is_default = args.flags.default === true
|
|
75
|
+
|
|
76
|
+
const [errors, data] = await groups_api.create_group(workspace, name, description, is_default)
|
|
77
|
+
if (errors) throw errors[errors.length - 1]
|
|
78
|
+
|
|
79
|
+
if (args.flags.json) {
|
|
80
|
+
print_json({ data })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
print_success(`Created group ${name}`)
|
|
85
|
+
print_hint(`Run happyskills groups show -w ${workspace} ${name} to see group details`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const delete_group = (args) => catch_errors('Delete group failed', async () => {
|
|
89
|
+
const workspace = require_workspace(args)
|
|
90
|
+
const name = args._[1]
|
|
91
|
+
if (!name) throw new UsageError('Group name is required. Usage: happyskills groups delete -w <workspace> <name>')
|
|
92
|
+
|
|
93
|
+
if (!args.flags.yes) {
|
|
94
|
+
const { confirm_prompt } = require('../utils/prompt')
|
|
95
|
+
const confirmed = await confirm_prompt(`Delete group ${name}? This will also remove all skill access grants.`)
|
|
96
|
+
if (!confirmed) {
|
|
97
|
+
console.log('Cancelled.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [errors, data] = await groups_api.delete_group(workspace, name)
|
|
103
|
+
if (errors) throw errors[errors.length - 1]
|
|
104
|
+
|
|
105
|
+
if (args.flags.json) {
|
|
106
|
+
print_json({ data })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
print_success(`Deleted group ${name}`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const show_group = (args) => catch_errors('Show group failed', async () => {
|
|
114
|
+
const workspace = require_workspace(args)
|
|
115
|
+
const name = args._[1]
|
|
116
|
+
if (!name) throw new UsageError('Group name is required. Usage: happyskills groups show -w <workspace> <name>')
|
|
117
|
+
|
|
118
|
+
const [errors, group] = await groups_api.show_group(workspace, name)
|
|
119
|
+
if (errors) throw errors[errors.length - 1]
|
|
120
|
+
|
|
121
|
+
if (args.flags.json) {
|
|
122
|
+
print_json({ data: group })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
print_label('Group', group.name)
|
|
127
|
+
print_label('Description', group.description || '(none)')
|
|
128
|
+
print_label('Default', group.is_default ? 'yes' : 'no')
|
|
129
|
+
|
|
130
|
+
const members = group.members || []
|
|
131
|
+
console.log()
|
|
132
|
+
print_label(`Members (${members.length})`, '')
|
|
133
|
+
if (members.length === 0) {
|
|
134
|
+
console.log(' (none)')
|
|
135
|
+
} else {
|
|
136
|
+
for (const m of members) {
|
|
137
|
+
console.log(` ${m.username} ${m.email || ''}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const add_member = (args) => catch_errors('Add group member failed', async () => {
|
|
143
|
+
const workspace = require_workspace(args)
|
|
144
|
+
const group = args._[1]
|
|
145
|
+
const username = args._[2]
|
|
146
|
+
if (!group || !username) throw new UsageError('Group and username are required. Usage: happyskills groups add -w <workspace> <group> <username>')
|
|
147
|
+
|
|
148
|
+
const [errors, data] = await groups_api.add_member(workspace, group, username)
|
|
149
|
+
if (errors) throw errors[errors.length - 1]
|
|
150
|
+
|
|
151
|
+
if (args.flags.json) {
|
|
152
|
+
print_json({ data })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
print_success(`Added ${username} to group ${group}`)
|
|
157
|
+
print_hint(`Run happyskills groups show -w ${workspace} ${group} to see group details`)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const remove_member = (args) => catch_errors('Remove group member failed', async () => {
|
|
161
|
+
const workspace = require_workspace(args)
|
|
162
|
+
const group = args._[1]
|
|
163
|
+
const username = args._[2]
|
|
164
|
+
if (!group || !username) throw new UsageError('Group and username are required. Usage: happyskills groups remove -w <workspace> <group> <username>')
|
|
165
|
+
|
|
166
|
+
const [errors, data] = await groups_api.remove_member(workspace, group, username)
|
|
167
|
+
if (errors) throw errors[errors.length - 1]
|
|
168
|
+
|
|
169
|
+
if (args.flags.json) {
|
|
170
|
+
print_json({ data })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
print_success(`Removed ${username} from group ${group}`)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const toggle_default = (args) => catch_errors('Toggle default failed', async () => {
|
|
178
|
+
const workspace = require_workspace(args)
|
|
179
|
+
const name = args._[1]
|
|
180
|
+
if (!name) throw new UsageError('Group name is required. Usage: happyskills groups default -w <workspace> <name> --on|--off')
|
|
181
|
+
|
|
182
|
+
const on = args.flags.on === true
|
|
183
|
+
const off = args.flags.off === true
|
|
184
|
+
if (!on && !off) throw new UsageError('Either --on or --off is required. Usage: happyskills groups default -w <workspace> <name> --on|--off')
|
|
185
|
+
if (on && off) throw new UsageError('Cannot use both --on and --off')
|
|
186
|
+
|
|
187
|
+
const is_default = on
|
|
188
|
+
const [errors, data] = await groups_api.update_group(workspace, name, { is_default })
|
|
189
|
+
if (errors) throw errors[errors.length - 1]
|
|
190
|
+
|
|
191
|
+
if (args.flags.json) {
|
|
192
|
+
print_json({ data })
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
print_success(is_default ? `${name} is now a default group` : `${name} is no longer a default group`)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const run = (args) => catch_errors('Groups command failed', async () => {
|
|
200
|
+
if (args.flags._show_help) {
|
|
201
|
+
print_help(HELP_TEXT)
|
|
202
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await require_token()
|
|
206
|
+
|
|
207
|
+
const sub = args._[0]
|
|
208
|
+
if (!sub || sub === 'list') return list_groups_cmd(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
209
|
+
if (sub === 'create') return create_group(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
210
|
+
if (sub === 'delete') return delete_group(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
211
|
+
if (sub === 'show') return show_group(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
212
|
+
if (sub === 'add') return add_member(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
213
|
+
if (sub === 'remove') return remove_member(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
214
|
+
if (sub === 'default') return toggle_default(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
215
|
+
|
|
216
|
+
throw new UsageError(`Unknown subcommand: ${sub}. Run happyskills groups --help`)
|
|
217
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
218
|
+
|
|
219
|
+
module.exports = { run }
|
package/src/commands/init.js
CHANGED
|
@@ -2,26 +2,29 @@ const path = require('path')
|
|
|
2
2
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
3
3
|
const { write_manifest } = require('../manifest/writer')
|
|
4
4
|
const { write_file, file_exists, ensure_dir } = require('../utils/fs')
|
|
5
|
-
const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
|
|
5
|
+
const { print_success, print_error, print_help, print_hint, print_json, print_info, print_warn, code } = require('../ui/output')
|
|
6
6
|
const { exit_with_error, CliError, UsageError } = require('../utils/errors')
|
|
7
7
|
const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
|
|
8
8
|
const { find_project_root, skills_dir } = require('../config/paths')
|
|
9
|
+
const { resolve_agents, link_to_agents } = require('../agents')
|
|
9
10
|
|
|
10
11
|
const HELP_TEXT = `Usage: happyskills init <name> [options]
|
|
11
12
|
|
|
12
|
-
Scaffold a new skill in .
|
|
13
|
+
Scaffold a new skill in .agents/skills/<name>/.
|
|
13
14
|
|
|
14
15
|
Creates:
|
|
15
|
-
.
|
|
16
|
-
.
|
|
16
|
+
.agents/skills/<name>/skill.json Skill manifest (name, version, deps)
|
|
17
|
+
.agents/skills/<name>/SKILL.md Skill instructions for AI agents
|
|
17
18
|
|
|
18
19
|
Arguments:
|
|
19
20
|
name Skill name (required, e.g., my-deploy-skill)
|
|
20
21
|
|
|
21
22
|
Options:
|
|
22
|
-
-g, --global
|
|
23
|
-
--kit
|
|
24
|
-
--
|
|
23
|
+
-g, --global Create in global skills (~/.agents/skills/)
|
|
24
|
+
--kit Initialize as a kit (collection of skills)
|
|
25
|
+
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
26
|
+
Default: auto-detect. Env: HAPPYSKILLS_AGENTS
|
|
27
|
+
--json Output as JSON
|
|
25
28
|
|
|
26
29
|
Examples:
|
|
27
30
|
happyskills init my-deploy-skill
|
|
@@ -104,14 +107,31 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
104
107
|
|
|
105
108
|
const label = is_kit ? 'kit' : 'skill'
|
|
106
109
|
|
|
110
|
+
// Link to detected agents (non-fatal — warnings only)
|
|
111
|
+
const linked_agents = []
|
|
112
|
+
const [agents_err, agents_result] = await resolve_agents(args.flags.agents || undefined)
|
|
113
|
+
if (!agents_err && agents_result?.agents?.length > 0) {
|
|
114
|
+
const [link_errs] = await link_to_agents(dir, agents_result.agents, { global: is_global, project_root, skill_name: final_name })
|
|
115
|
+
if (link_errs) {
|
|
116
|
+
print_warn(`Warning: failed to link ${final_name} to some agents`)
|
|
117
|
+
} else {
|
|
118
|
+
linked_agents.push(...agents_result.agents.map(a => a.id))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
if (args.flags.json) {
|
|
108
|
-
|
|
123
|
+
const data = { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir }
|
|
124
|
+
if (linked_agents.length > 0) data.linked_agents = linked_agents
|
|
125
|
+
print_json({ data })
|
|
109
126
|
return
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
print_success(`Initialized ${label} "${final_name}" at ${dir}`)
|
|
113
130
|
console.log(` ${SKILL_JSON} — manifest`)
|
|
114
131
|
console.log(` ${SKILL_MD} — ${is_kit ? 'kit description' : 'instructions'}`)
|
|
132
|
+
if (linked_agents.length > 0) {
|
|
133
|
+
print_info(`Linked to: ${agents_result.agents.map(a => a.display_name).join(', ')}`)
|
|
134
|
+
}
|
|
115
135
|
console.log()
|
|
116
136
|
print_hint(`Edit these files, then run ${code('happyskills publish')} to share.`)
|
|
117
137
|
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
2
|
+
const { require_token } = require('../auth/token_store')
|
|
3
|
+
const members_api = require('../api/members')
|
|
4
|
+
const users_api = require('../api/users')
|
|
5
|
+
const { print_help, print_json, print_table, print_success, print_hint } = require('../ui/output')
|
|
6
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
7
|
+
const { EXIT_CODES } = require('../constants')
|
|
8
|
+
|
|
9
|
+
const HELP_TEXT = `Usage: happyskills people <subcommand> [options]
|
|
10
|
+
|
|
11
|
+
Manage workspace membership.
|
|
12
|
+
|
|
13
|
+
Subcommands:
|
|
14
|
+
list List workspace members
|
|
15
|
+
add <username> Add a person to the workspace
|
|
16
|
+
remove <username> Remove a person from the workspace
|
|
17
|
+
role <username> <role> Change a person's role
|
|
18
|
+
search <query> Search for users
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-w, --workspace <slug> Workspace slug (required for list, add, remove, role)
|
|
22
|
+
--role <role> Role for add: owner, admin, member, viewer (default: member)
|
|
23
|
+
--limit <n> Max search results (default: 10)
|
|
24
|
+
--json Output as JSON
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
happyskills people list -w acme
|
|
28
|
+
happyskills people add -w acme alice --role admin
|
|
29
|
+
happyskills people remove -w acme alice
|
|
30
|
+
happyskills people role -w acme alice admin
|
|
31
|
+
happyskills people search alice`
|
|
32
|
+
|
|
33
|
+
const require_workspace = (args) => {
|
|
34
|
+
const workspace = args.flags.workspace
|
|
35
|
+
if (!workspace) throw new UsageError('--workspace (-w) is required')
|
|
36
|
+
return workspace
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const list_people = (args) => catch_errors('List people failed', async () => {
|
|
40
|
+
const workspace = require_workspace(args)
|
|
41
|
+
const [errors, members] = await members_api.list_members(workspace)
|
|
42
|
+
if (errors) throw errors[errors.length - 1]
|
|
43
|
+
|
|
44
|
+
if (args.flags.json) {
|
|
45
|
+
print_json({ data: members })
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!members || members.length === 0) {
|
|
50
|
+
console.log('No members found.')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
print_table(
|
|
55
|
+
['Username', 'Email', 'Role', 'Joined'],
|
|
56
|
+
members.map(m => [m.username, m.email || '', m.role, m.created_at ? m.created_at.slice(0, 10) : ''])
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const add_person = (args) => catch_errors('Add person failed', async () => {
|
|
61
|
+
const workspace = require_workspace(args)
|
|
62
|
+
const username = args._[1]
|
|
63
|
+
if (!username) throw new UsageError('Username is required. Usage: happyskills people add -w <workspace> <username>')
|
|
64
|
+
const role = args.flags.role || 'member'
|
|
65
|
+
|
|
66
|
+
const [errors, data] = await members_api.add_member(workspace, username, role)
|
|
67
|
+
if (errors) throw errors[errors.length - 1]
|
|
68
|
+
|
|
69
|
+
if (args.flags.json) {
|
|
70
|
+
print_json({ data })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
print_success(`Added ${username} to ${workspace} as ${role}`)
|
|
75
|
+
print_hint(`Run happyskills people list -w ${workspace} to see all members`)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const remove_person = (args) => catch_errors('Remove person failed', async () => {
|
|
79
|
+
const workspace = require_workspace(args)
|
|
80
|
+
const username = args._[1]
|
|
81
|
+
if (!username) throw new UsageError('Username is required. Usage: happyskills people remove -w <workspace> <username>')
|
|
82
|
+
|
|
83
|
+
if (!args.flags.yes) {
|
|
84
|
+
const { confirm_prompt } = require('../utils/prompt')
|
|
85
|
+
const confirmed = await confirm_prompt(`Remove ${username} from ${workspace}?`)
|
|
86
|
+
if (!confirmed) {
|
|
87
|
+
console.log('Cancelled.')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [errors, data] = await members_api.remove_member(workspace, username)
|
|
93
|
+
if (errors) throw errors[errors.length - 1]
|
|
94
|
+
|
|
95
|
+
if (args.flags.json) {
|
|
96
|
+
print_json({ data })
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
print_success(`Removed ${username} from ${workspace}`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const change_role = (args) => catch_errors('Change role failed', async () => {
|
|
104
|
+
const workspace = require_workspace(args)
|
|
105
|
+
const username = args._[1]
|
|
106
|
+
const role = args._[2]
|
|
107
|
+
if (!username || !role) throw new UsageError('Username and role are required. Usage: happyskills people role -w <workspace> <username> <role>')
|
|
108
|
+
|
|
109
|
+
const [errors, data] = await members_api.update_role(workspace, username, role)
|
|
110
|
+
if (errors) throw errors[errors.length - 1]
|
|
111
|
+
|
|
112
|
+
if (args.flags.json) {
|
|
113
|
+
print_json({ data })
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
print_success(`Changed ${username} role to ${role}`)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const search_people = (args) => catch_errors('Search users failed', async () => {
|
|
121
|
+
const query = args._[1]
|
|
122
|
+
if (!query) throw new UsageError('Search query is required. Usage: happyskills people search <query>')
|
|
123
|
+
const limit = args.flags.limit ? parseInt(args.flags.limit, 10) : 10
|
|
124
|
+
|
|
125
|
+
const [errors, users] = await users_api.search_users(query, limit)
|
|
126
|
+
if (errors) throw errors[errors.length - 1]
|
|
127
|
+
|
|
128
|
+
if (args.flags.json) {
|
|
129
|
+
print_json({ data: users })
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!users || users.length === 0) {
|
|
134
|
+
console.log('No users found.')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
print_table(
|
|
139
|
+
['Username', 'Email', 'Name'],
|
|
140
|
+
users.map(u => [u.username, u.email || '', u.name || ''])
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const run = (args) => catch_errors('People command failed', async () => {
|
|
145
|
+
if (args.flags._show_help) {
|
|
146
|
+
print_help(HELP_TEXT)
|
|
147
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await require_token()
|
|
151
|
+
|
|
152
|
+
const sub = args._[0]
|
|
153
|
+
if (!sub || sub === 'list') return list_people(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
154
|
+
if (sub === 'add') return add_person(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
155
|
+
if (sub === 'remove') return remove_person(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
156
|
+
if (sub === 'role') return change_role(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
157
|
+
if (sub === 'search') return search_people(args).then(([errors]) => { if (errors) throw errors[errors.length - 1] })
|
|
158
|
+
|
|
159
|
+
throw new UsageError(`Unknown subcommand: ${sub}. Run happyskills people --help`)
|
|
160
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
161
|
+
|
|
162
|
+
module.exports = { run }
|
package/src/constants.js
CHANGED
|
@@ -37,7 +37,8 @@ const COMMAND_ALIASES = {
|
|
|
37
37
|
pub: 'publish',
|
|
38
38
|
v: 'validate',
|
|
39
39
|
del: 'delete',
|
|
40
|
-
vis: 'visibility'
|
|
40
|
+
vis: 'visibility',
|
|
41
|
+
grp: 'groups'
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const COMMANDS = [
|
|
@@ -64,7 +65,10 @@ const COMMANDS = [
|
|
|
64
65
|
'setup',
|
|
65
66
|
'validate',
|
|
66
67
|
'self-update',
|
|
67
|
-
'config'
|
|
68
|
+
'config',
|
|
69
|
+
'people',
|
|
70
|
+
'groups',
|
|
71
|
+
'access'
|
|
68
72
|
]
|
|
69
73
|
|
|
70
74
|
module.exports = {
|
package/src/index.js
CHANGED
|
@@ -102,6 +102,9 @@ Commands:
|
|
|
102
102
|
fork <owner/skill> Fork a skill to your workspace
|
|
103
103
|
setup Install the HappySkills CLI skill globally
|
|
104
104
|
self-update Upgrade happyskills CLI to latest version
|
|
105
|
+
people <sub> Manage workspace members (list, add, remove, role, search)
|
|
106
|
+
groups <sub> Manage workspace groups (list, create, delete, show, add, remove, default)
|
|
107
|
+
access <sub> Manage group skill access (list, grant, revoke, set)
|
|
105
108
|
login Authenticate with the registry
|
|
106
109
|
logout Clear stored credentials
|
|
107
110
|
whoami Show current user
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { createInterface } = require('readline')
|
|
2
|
+
|
|
3
|
+
const confirm_prompt = (message) => new Promise((resolve) => {
|
|
4
|
+
if (!process.stdin.isTTY) {
|
|
5
|
+
resolve(false)
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
10
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
11
|
+
rl.close()
|
|
12
|
+
resolve(answer.trim().toLowerCase() === 'y')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
module.exports = { confirm_prompt }
|
|
@@ -74,7 +74,19 @@ const validate_description = (fm) => {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (desc.length > 1024) {
|
|
77
|
-
|
|
77
|
+
const deficit = desc.length - 1024
|
|
78
|
+
results.push({
|
|
79
|
+
...result('description', 'max_length', 'error', `Description is ${desc.length} chars (max 1024). Must reduce by ${deficit} chars.`, desc),
|
|
80
|
+
recommendations: [
|
|
81
|
+
'STEP 1 - AUDIT: Before changing anything, read the skill\'s routing table or capability list. Map each phrase in the description to the capability it triggers. Mark each phrase as: IDENTITY (describes what the skill is, usually one phrase), UNIQUE (the only phrase matching a specific capability), or REINFORCING (overlaps with another phrase\'s coverage).',
|
|
82
|
+
'STEP 2 - LOSSLESS COMPRESSION: Apply these transformations that reduce characters without changing semantic meaning: (a) Remove articles (a, an, the). (b) Remove possessives (my, your) when the subject is implied. (c) Remove filler verbs (do, does, can, have, is, am). (d) Merge parallel structures that share the same verb (e.g., \'install kit. publish kit\' becomes \'install, publish kits\') or the same object (e.g., \'find kits, search kits\' becomes \'find, search kits\'). Stop here if now under the limit.',
|
|
83
|
+
'STEP 3 - LOSSY COMPRESSION (only if still over the limit after step 2): Remove REINFORCING phrases only. When two phrases overlap semantically, remove the more general one and keep the more specific one. In synonym clusters, keep the two most commonly used verbs and remove the rest.',
|
|
84
|
+
'NEVER remove an IDENTITY phrase — it anchors the understanding of the skill\'s purpose and provides context for all other trigger phrases.',
|
|
85
|
+
'NEVER remove a UNIQUE trigger phrase — it is the only semantic path to that capability. Removing it makes that capability unreachable via auto-invocation.',
|
|
86
|
+
'NEVER rephrase a trigger in a way that changes the core verb or noun (e.g., \'how many skills installed\' must not become \'skill count\'). The original phrasing matches how users naturally speak.',
|
|
87
|
+
'STEP 4 - VERIFY: Cross-check the shortened description against the skill\'s routing table or capability list. Every documented capability must still have at least one semantically matching phrase in the description. If any capability lost coverage, restore its trigger phrase and find different savings.'
|
|
88
|
+
]
|
|
89
|
+
})
|
|
78
90
|
} else {
|
|
79
91
|
results.push(result('description', 'max_length', 'pass', `Description: ${desc.length} chars (max 1024)`))
|
|
80
92
|
}
|