happyskills 0.4.4 → 0.6.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,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-03-06
11
+
12
+ ### Added
13
+ - Add `--public` flag to `publish` command to explicitly publish a skill as publicly discoverable in the catalog
14
+
15
+ ### Changed
16
+ - Change `publish` default visibility from public to private — skills are now private by default; pass `--public` to make a skill visible in the catalog
17
+ - Change `convert` to no longer auto-publish to the registry after converting; the skill is registered locally and the user is prompted to run `happyskills publish <skill-name>` as a separate step
18
+
19
+ ### Fixed
20
+ - Fix `--version` flag collision where commands accepting `--version <value>` (e.g., `convert --version 1.0.0`) would incorrectly trigger the root `--version` output instead of passing the value to the command
21
+
22
+ ## [0.5.0] - 2026-03-05
23
+
24
+ ### Added
25
+ - Add `--mine` flag to `search` command to browse skills across all user's workspaces
26
+ - Add `--personal` flag to `search` command to browse skills in the user's personal workspace only
27
+ - Add `-w` / `--workspace <slug>` flag to `search` command to search within specific workspace(s)
28
+ - Add `--tags <tags>` flag to `search` command to filter results by tags
29
+ - Add `scope` and `visibility` fields to `search` JSON output
30
+
31
+ ### Changed
32
+ - Change `search` query argument from required to optional when `--mine`, `--personal`, or `--workspace` is provided (browse mode)
33
+
10
34
  ## [0.4.4] - 2026-03-05
11
35
 
12
36
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.4.4",
3
+ "version": "0.6.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)",
package/src/api/push.js CHANGED
@@ -12,13 +12,13 @@ const estimate_payload_size = (files) => {
12
12
  return size
13
13
  }
14
14
 
15
- const smart_push = (owner, repo, { version, message, files }, on_progress) =>
15
+ const smart_push = (owner, repo, { version, message, files, visibility }, on_progress) =>
16
16
  catch_errors('Smart push failed', async () => {
17
17
  const payload_size = estimate_payload_size(files)
18
18
 
19
19
  if (payload_size < DIRECT_PUSH_THRESHOLD) {
20
20
  // Small payload — use direct push
21
- const [err, data] = await repos_api.push(owner, repo, { version, message, files })
21
+ const [err, data] = await repos_api.push(owner, repo, { version, message, files, visibility })
22
22
  if (err) throw e('Direct push failed', err)
23
23
  return data
24
24
  }
@@ -28,7 +28,7 @@ const smart_push = (owner, repo, { version, message, files }, on_progress) =>
28
28
 
29
29
  // Step 1: Initiate
30
30
  const [init_err, init_data] = await initiate_upload(owner, repo, {
31
- version, message, files: file_meta
31
+ version, message, files: file_meta, visibility
32
32
  })
33
33
  if (init_err) throw e('Upload initiation failed', init_err)
34
34
 
package/src/api/repos.js CHANGED
@@ -2,10 +2,15 @@ const { error: { catch_errors } } = require('puffy-core')
2
2
  const client = require('./client')
3
3
 
4
4
  const search = (query, options = {}) => catch_errors('Search failed', async () => {
5
- const params = new URLSearchParams({ q: query })
5
+ const params = new URLSearchParams()
6
+ if (query) params.set('q', query)
6
7
  if (options.limit) params.set('limit', options.limit)
7
8
  if (options.offset) params.set('offset', options.offset)
8
- const [errors, data] = await client.get(`/repos/search?${params}`, { auth: false })
9
+ if (options.scope) params.set('scope', options.scope)
10
+ if (options.workspace) params.set('workspace', options.workspace)
11
+ if (options.tags) params.set('tags', options.tags)
12
+ const needs_auth = (options.scope && options.scope !== 'public') || options.workspace
13
+ const [errors, data] = await client.get(`/repos/search?${params}`, { auth: needs_auth || false })
9
14
  if (errors) throw errors[errors.length - 1]
10
15
  return data
11
16
  })
@@ -5,9 +5,7 @@ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
5
5
  const { require_token } = require('../auth/token_store')
6
6
  const repos_api = require('../api/repos')
7
7
  const workspaces_api = require('../api/workspaces')
8
- const { smart_push } = require('../api/push')
9
8
  const { parse_frontmatter } = require('../utils/skill_scanner')
10
- const { collect_files } = require('../utils/file_collector')
11
9
  const { write_manifest } = require('../manifest/writer')
12
10
  const { read_lock } = require('../lock/reader')
13
11
  const { write_lock, update_lock_skills } = require('../lock/writer')
@@ -125,7 +123,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
125
123
  console.error(` Description: ${description || '(none)'}`)
126
124
  console.error(` Keywords: ${keywords.length ? keywords.join(', ') : '(none)'}`)
127
125
  console.error('')
128
- const answer = await confirm('Publish this skill to the registry? [y/N] ')
126
+ const answer = await confirm('Convert this skill to a managed package? [y/N] ')
129
127
  if (answer !== 'y' && answer !== 'yes') {
130
128
  print_info('Aborted.')
131
129
  return process.exit(EXIT_CODES.SUCCESS)
@@ -138,21 +136,6 @@ const run = (args) => catch_errors('Convert failed', async () => {
138
136
  const [manifest_err] = await write_manifest(skill_dir, manifest)
139
137
  if (manifest_err) { pub_spinner.fail('Failed to write skill.json'); throw e('Failed to write skill.json', manifest_err) }
140
138
 
141
- pub_spinner.update('Packaging skill...')
142
- const [collect_err, skill_files] = await collect_files(skill_dir)
143
- if (collect_err) { pub_spinner.fail('Failed to collect files'); throw e('File collection failed', collect_err) }
144
-
145
- pub_spinner.update(`Publishing ${workspace.slug}/${skill_name}@${version}...`)
146
- const on_progress = (completed, total) => {
147
- pub_spinner.update(`Uploading files (${completed}/${total})...`)
148
- }
149
- const [push_err, push_data] = await smart_push(workspace.slug, skill_name, {
150
- version,
151
- message: `Release ${version}`,
152
- files: skill_files
153
- }, on_progress)
154
- if (push_err) { pub_spinner.fail('Publish failed'); throw e('Push failed', push_err) }
155
-
156
139
  pub_spinner.update('Updating lock file...')
157
140
  const lock_dir = lock_root(is_global, project_root)
158
141
  const [, lock_data] = await read_lock(lock_dir)
@@ -162,8 +145,8 @@ const run = (args) => catch_errors('Convert failed', async () => {
162
145
  const updates = {
163
146
  [full_name]: {
164
147
  version,
165
- ref: push_data?.ref || `refs/tags/v${version}`,
166
- commit: push_data?.commit || null,
148
+ ref: null,
149
+ commit: null,
167
150
  integrity: integrity || null,
168
151
  requested_by: ['__root__'],
169
152
  dependencies: {}
@@ -185,6 +168,9 @@ const run = (args) => catch_errors('Convert failed', async () => {
185
168
  } })
186
169
  return
187
170
  }
171
+
172
+ console.error('')
173
+ print_info(`Next step: enrich metadata, then run \`happyskills publish ${skill_name}\` to publish.`)
188
174
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
189
175
 
190
176
  module.exports = { run }
@@ -24,6 +24,7 @@ Arguments:
24
24
  Options:
25
25
  --bump <type> Auto-bump version before publishing (patch, minor, major)
26
26
  --workspace <slug> Target workspace (overrides lock file owner)
27
+ --public Publish as public (default is private)
27
28
 
28
29
  Aliases: pub
29
30
 
@@ -120,10 +121,12 @@ const run = (args) => catch_errors('Publish failed', async () => {
120
121
  const on_progress = (completed, total) => {
121
122
  spinner.update(`Uploading files (${completed}/${total})...`)
122
123
  }
124
+ const visibility = args.flags.public ? 'public' : 'private'
123
125
  const [push_err] = await smart_push(workspace.slug, manifest.name, {
124
126
  version: manifest.version,
125
127
  message: `Release ${manifest.version}`,
126
- files: skill_files
128
+ files: skill_files,
129
+ visibility
127
130
  }, on_progress)
128
131
  if (push_err) { spinner.fail('Publish failed'); throw e('Push failed', push_err) }
129
132
 
@@ -1,24 +1,31 @@
1
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
2
  const repos_api = require('../api/repos')
3
3
  const { print_help, print_table, print_json, print_info } = require('../ui/output')
4
- const { exit_with_error, UsageError } = require('../utils/errors')
4
+ const { exit_with_error, UsageError, AuthError } = require('../utils/errors')
5
5
  const { EXIT_CODES } = require('../constants')
6
+ const { load_token } = require('../auth/token_store')
6
7
 
7
- const HELP_TEXT = `Usage: happyskills search <query> [options]
8
+ const HELP_TEXT = `Usage: happyskills search [query] [options]
8
9
 
9
10
  Search the skill registry.
10
11
 
11
12
  Arguments:
12
- query Search term
13
+ query Search term (optional with --mine, --personal, or --workspace)
13
14
 
14
15
  Options:
15
- --json Output as JSON
16
+ -w, --workspace <slug> Search within specific workspace(s) (comma-separated)
17
+ --mine Search across all your workspaces
18
+ --personal Search only your personal workspace
19
+ --tags <tags> Filter by tags (comma-separated)
20
+ --json Output as JSON
16
21
 
17
22
  Aliases: s
18
23
 
19
24
  Examples:
20
25
  happyskills search deploy
21
- happyskills s aws --json`
26
+ happyskills search --mine
27
+ happyskills search deploy --workspace acme
28
+ happyskills s --personal --json`
22
29
 
23
30
  const run = (args) => catch_errors('Search failed', async () => {
24
31
  if (args.flags._show_help) {
@@ -26,32 +33,52 @@ const run = (args) => catch_errors('Search failed', async () => {
26
33
  return process.exit(EXIT_CODES.SUCCESS)
27
34
  }
28
35
 
29
- const query = args._.join(' ')
30
- if (!query) {
31
- throw new UsageError('Please provide a search query (e.g., happyskills search deploy).')
36
+ const query = args._.join(' ') || null
37
+ const { mine, personal, workspace, tags } = args.flags
38
+ const has_scope_flag = mine || personal || workspace
39
+
40
+ if (!query && !has_scope_flag && !tags) {
41
+ throw new UsageError('Please provide a search query or use --mine, --personal, or --workspace.')
42
+ }
43
+
44
+ const scope = mine ? 'mine' : personal ? 'personal' : undefined
45
+
46
+ if (has_scope_flag) {
47
+ const [, token_data] = await load_token()
48
+ if (!token_data) {
49
+ throw new AuthError('Authentication required. Run `happyskills login` first.')
50
+ }
32
51
  }
33
52
 
34
- const [errors, results] = await repos_api.search(query)
53
+ const options = {}
54
+ if (scope) options.scope = scope
55
+ if (workspace) options.workspace = workspace
56
+ if (tags) options.tags = tags
57
+
58
+ const [errors, results] = await repos_api.search(query, options)
35
59
  if (errors) throw e('Search failed', errors)
36
60
 
37
61
  const items = Array.isArray(results) ? results : (results?.repos || results?.items || [])
62
+ const effective_scope = scope || (has_scope_flag ? undefined : undefined)
38
63
 
39
64
  if (items.length === 0) {
40
65
  if (args.flags.json) {
41
- print_json({ data: { query, results: [], count: 0 } })
66
+ print_json({ data: { query, scope: effective_scope || 'all', results: [], count: 0 } })
42
67
  return
43
68
  }
44
- print_info(`No skills found for "${query}".`)
69
+ const context = query ? ` for "${query}"` : ''
70
+ print_info(`No skills found${context}.`)
45
71
  return
46
72
  }
47
73
 
48
74
  if (args.flags.json) {
49
- const results = items.map(item => ({
75
+ const mapped = items.map(item => ({
50
76
  skill: `${item.owner || item.workspace_slug}/${item.name}`,
51
77
  description: item.description || '',
52
- version: item.latest_version || item.version || '-'
78
+ version: item.latest_version || item.version || '-',
79
+ visibility: item.visibility || 'public'
53
80
  }))
54
- print_json({ data: { query, results, count: results.length } })
81
+ print_json({ data: { query, scope: effective_scope || 'all', results: mapped, count: mapped.length } })
55
82
  return
56
83
  }
57
84
 
package/src/index.js CHANGED
@@ -62,6 +62,7 @@ const parse_args = (argv) => {
62
62
  const SHORT_FLAGS = {
63
63
  g: 'global',
64
64
  y: 'yes',
65
+ w: 'workspace',
65
66
  }
66
67
 
67
68
  const normalize_flags = (flags) => {
@@ -114,7 +115,7 @@ const run = (argv) => {
114
115
  set_json_mode()
115
116
  }
116
117
 
117
- if (args.flags.version) {
118
+ if (args.flags.version === true) {
118
119
  show_version()
119
120
  return process.exit(EXIT_CODES.SUCCESS)
120
121
  }