happyskills 0.25.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 CHANGED
@@ -7,6 +7,17 @@ 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
+
10
21
  ## [0.25.0] - 2026-03-30
11
22
 
12
23
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -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 }
@@ -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 }
@@ -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 { require_token } = require('../auth/token_store')
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 require_token()
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
- const spinner = create_spinner('Resolving workspace...')
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
- const [ws_err, workspaces] = await workspaces_api.list_workspaces()
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
- const workspace = choose_workspace(workspaces, args.flags.workspace)
131
-
132
- spinner.update('Checking registry...')
133
- const [repo_err] = await repos_api.get_repo(workspace.slug, skill_name, { auth: true })
134
- if (!repo_err) {
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
- 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)
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 }
@@ -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 }