happyskills 0.25.0 → 0.27.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 +16 -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 +21 -13
- package/src/commands/groups.js +219 -0
- package/src/commands/list.js +20 -4
- 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/utils/skill_scanner.js +77 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.27.0] - 2026-04-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add agent-orphan skill detection to `list` command — scans all agent-specific skill directories (`.claude/skills/`, `.cursor/skills/`, etc.) for skills placed directly in agent folders without going through HappySkills, and reports them as `agent_orphans` in JSON output or `agent-orphan (<agent>)` source in table output
|
|
14
|
+
|
|
15
|
+
## [0.26.0] - 2026-04-01
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Add `people` command with subcommands: `list`, `add`, `remove`, `role`, `search` for workspace membership management
|
|
19
|
+
- Add `groups` command (alias: `grp`) with subcommands: `list`, `create`, `delete`, `show`, `add`, `remove`, `default` for workspace group management
|
|
20
|
+
- Add `access` command with subcommands: `list`, `grant`, `revoke`, `set` for group-level skill permission management
|
|
21
|
+
- Add `confirm_prompt` utility for interactive confirmation on destructive operations (`people remove`, `groups delete`)
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Make `convert` authentication optional — when not logged in, `--workspace <slug>` is required and the registry conflict check is skipped
|
|
25
|
+
|
|
10
26
|
## [0.25.0] - 2026-03-30
|
|
11
27
|
|
|
12
28
|
### 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')
|
|
@@ -72,7 +72,8 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
72
72
|
throw new UsageError('Please specify a skill name (e.g., happyskills convert my-skill).')
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
await
|
|
75
|
+
const [, token_data] = await load_token()
|
|
76
|
+
const is_authenticated = !!token_data
|
|
76
77
|
|
|
77
78
|
const is_global = args.flags.global || false
|
|
78
79
|
const project_root = find_project_root()
|
|
@@ -122,22 +123,29 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
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) }
|
|
126
131
|
|
|
127
|
-
|
|
128
|
-
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)
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|
|
135
140
|
spinner.stop()
|
|
136
|
-
|
|
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.')
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
spinner.stop()
|
|
140
|
-
|
|
141
149
|
if (!args.flags.yes && !args.flags.json) {
|
|
142
150
|
console.error('')
|
|
143
151
|
console.error(` Skill: ${skill_name}`)
|
|
@@ -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/list.js
CHANGED
|
@@ -2,7 +2,8 @@ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
|
2
2
|
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
3
3
|
const { skills_dir, skill_install_dir, find_project_root, lock_root } = require('../config/paths')
|
|
4
4
|
const { file_exists, read_json } = require('../utils/fs')
|
|
5
|
-
const { scan_skills_dir } = require('../utils/skill_scanner')
|
|
5
|
+
const { scan_skills_dir, scan_agent_orphan_skills } = require('../utils/skill_scanner')
|
|
6
|
+
const { AGENTS } = require('../agents/registry')
|
|
6
7
|
const { print_help, print_table, print_json, print_info } = require('../ui/output')
|
|
7
8
|
const { exit_with_error } = require('../utils/errors')
|
|
8
9
|
const { EXIT_CODES, SKILL_JSON, SKILL_TYPES } = require('../constants')
|
|
@@ -40,9 +41,14 @@ const run = (args) => catch_errors('List failed', async () => {
|
|
|
40
41
|
const managed_names = new Set(managed_entries.map(([k]) => k.split('/')[1]))
|
|
41
42
|
const external_skills = (disk_skills || []).filter(s => !managed_names.has(s.name))
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
// Scan agent-specific dirs for skills placed directly in agent folders (not symlinked from .agents/skills/)
|
|
45
|
+
const all_known_names = new Set([...managed_names, ...external_skills.map(s => s.name)])
|
|
46
|
+
const [, agent_orphans] = await scan_agent_orphan_skills(AGENTS, is_global, project_root, all_known_names)
|
|
47
|
+
const orphan_skills = agent_orphans || []
|
|
48
|
+
|
|
49
|
+
if (managed_entries.length === 0 && external_skills.length === 0 && orphan_skills.length === 0) {
|
|
44
50
|
if (args.flags.json) {
|
|
45
|
-
print_json({ data: { skills: {}, external: [] } })
|
|
51
|
+
print_json({ data: { skills: {}, external: [], agent_orphans: [] } })
|
|
46
52
|
return
|
|
47
53
|
}
|
|
48
54
|
print_info('No skills installed.')
|
|
@@ -69,7 +75,12 @@ const run = (args) => catch_errors('List failed', async () => {
|
|
|
69
75
|
skills_map[name] = { version: data.version, type, source, status }
|
|
70
76
|
}
|
|
71
77
|
const external = external_skills.map(s => ({ name: s.name, description: s.description || '' }))
|
|
72
|
-
|
|
78
|
+
const agent_orphan_list = orphan_skills.map(s => ({
|
|
79
|
+
name: s.name,
|
|
80
|
+
description: s.description || '',
|
|
81
|
+
agents: s.agents
|
|
82
|
+
}))
|
|
83
|
+
print_json({ data: { skills: skills_map, external, agent_orphans: agent_orphan_list } })
|
|
73
84
|
return
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -88,6 +99,11 @@ const run = (args) => catch_errors('List failed', async () => {
|
|
|
88
99
|
rows.push([s.name, '-', 'external', 'installed'])
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
for (const s of orphan_skills) {
|
|
103
|
+
const agent_label = s.agents.map(a => a.name).join(', ')
|
|
104
|
+
rows.push([s.name, '-', `agent-orphan (${agent_label})`, 'installed'])
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
print_table(['Skill', 'Version', 'Source', 'Status'], rows)
|
|
92
108
|
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
93
109
|
|
|
@@ -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 }
|
|
@@ -53,4 +53,80 @@ const scan_skills_dir = (base_dir) => catch_errors('Failed to scan skills direct
|
|
|
53
53
|
return skills
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Scans agent-specific skill directories for skills that are NOT symlinks
|
|
58
|
+
* back to the canonical .agents/skills/ directory. These are "agent-orphan"
|
|
59
|
+
* skills — placed directly in an agent folder without going through HappySkills.
|
|
60
|
+
*
|
|
61
|
+
* @param {Array} agents - Agent definitions from the registry (AGENTS array)
|
|
62
|
+
* @param {boolean} is_global - Whether to scan global or project-level dirs
|
|
63
|
+
* @param {string} project_root - Project root path
|
|
64
|
+
* @param {Set<string>} known_names - Names already tracked (managed + external from canonical dir)
|
|
65
|
+
* @returns {[Error|null, Array<{name:string, description:string, agent_id:string, agent_name:string}>]}
|
|
66
|
+
*/
|
|
67
|
+
const scan_agent_orphan_skills = (agents, is_global, project_root, known_names) => catch_errors('Failed to scan agent skill directories', async () => {
|
|
68
|
+
const orphans = []
|
|
69
|
+
|
|
70
|
+
for (const agent of agents) {
|
|
71
|
+
const agent_dir = is_global
|
|
72
|
+
? path.join(require('os').homedir(), agent.global_skills_dir)
|
|
73
|
+
: path.join(project_root || process.cwd(), agent.skills_dir)
|
|
74
|
+
|
|
75
|
+
let entries
|
|
76
|
+
try {
|
|
77
|
+
entries = await fs.promises.readdir(agent_dir, { withFileTypes: true })
|
|
78
|
+
} catch {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
84
|
+
if (entry.name.startsWith('.') || SKIP_ENTRIES.has(entry.name)) continue
|
|
85
|
+
|
|
86
|
+
// Skip symlinks — those are managed by HappySkills
|
|
87
|
+
if (entry.isSymbolicLink()) continue
|
|
88
|
+
|
|
89
|
+
// Skip skills already known from the canonical dir or lock file
|
|
90
|
+
if (known_names.has(entry.name)) continue
|
|
91
|
+
|
|
92
|
+
const dir = path.join(agent_dir, entry.name)
|
|
93
|
+
const skill_md_path = path.join(dir, 'SKILL.md')
|
|
94
|
+
|
|
95
|
+
let content
|
|
96
|
+
try {
|
|
97
|
+
content = await fs.promises.readFile(skill_md_path, 'utf-8')
|
|
98
|
+
} catch {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const frontmatter = parse_frontmatter(content)
|
|
103
|
+
if (!frontmatter || !frontmatter.name) continue
|
|
104
|
+
|
|
105
|
+
orphans.push({
|
|
106
|
+
name: frontmatter.name,
|
|
107
|
+
description: frontmatter.description || '',
|
|
108
|
+
agent_id: agent.id,
|
|
109
|
+
agent_name: agent.display_name
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Deduplicate — the same skill dir name might appear in multiple agent dirs.
|
|
115
|
+
// Keep the first occurrence and collect all agent IDs.
|
|
116
|
+
const by_name = new Map()
|
|
117
|
+
for (const s of orphans) {
|
|
118
|
+
if (by_name.has(s.name)) {
|
|
119
|
+
by_name.get(s.name).agents.push({ id: s.agent_id, name: s.agent_name })
|
|
120
|
+
} else {
|
|
121
|
+
by_name.set(s.name, {
|
|
122
|
+
name: s.name,
|
|
123
|
+
description: s.description,
|
|
124
|
+
agents: [{ id: s.agent_id, name: s.agent_name }]
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [...by_name.values()]
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
module.exports = { scan_skills_dir, scan_agent_orphan_skills, parse_frontmatter }
|